Security re-check and load test 2026-10-04: 32 threads, data file permissions, patched deps
- gunicorn gthread 8 -> 32 threads per worker: each assistant stream holds a thread for the whole answer; with 3x8, about 20 concurrent asks already queued ordinary page loads for seconds (up to 23 s at 50 asks locally, 7 s at 20 asks on the live server). With 3x32, 75 concurrent asks kept page loads under 110 ms (laptop, simulated model and Qdrant) - deploy.sh: new step closes the project dir to other accounts and sets the database and backups to 0600/0700 on every release; user `nobody` on the shared server could read db.sqlite3 and every backup - requirements: python-dotenv 1.2.2, urllib3>=2.8.0 (known CVEs in the installed versions), gunicorn pinned to the server's version (was only installed by hand) - security-before-production.md: items 19-23 (file permissions, DEBUG=1 still in the systemd unit, dependencies, frontend headers, guest AI cap per shared IP) and the reproduced X-Forwarded-For spoof through the origin IP - model-prices.md: 0.183 cents per answer from 50 answers in the live load test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز ماندهاند
|
||||
|
||||
**تاریخِ ثبت: ۲۰۲۶-۰۹-۲۵ (۳ مهر ۱۴۰۵)، کامیتِ پایه `66066cb`.**
|
||||
**تاریخِ ثبت: ۲۰۲۶-۰۹-۲۵ (۳ مهر ۱۴۰۵)، کامیتِ پایه `66066cb`.** بازبینیِ دوباره با سرورِ زنده:
|
||||
۲۰۲۶-۱۰-۰۴ (مواردِ ۱۹ تا ۲۳ و بازتولیدِ مورد ۸).
|
||||
|
||||
این پروژه تا این تاریخ در حالتِ **دمو و ارائهی کارآموزی** است، نه محصولِ نهایی. چند چیز
|
||||
عمداً باز گذاشته شده تا توسعهدهندهها، فرانتکار و ارائه کار کنند. هر کدام برای یک سرورِ
|
||||
@@ -38,6 +39,11 @@
|
||||
| ۱۶ | فرانت توکن را در `localStorage` نگه میدارد | کم | فرانت |
|
||||
| ۱۷ | ~~شمارهی موبایل نرمال نمیشود؛ سقفهای per-phone با املای دیگر دور زده میشوند~~ **بسته شد ۲۰۲۶-۰۹-۲۵** | متوسط | کد |
|
||||
| ۱۸ | `/api/payment/orders/{id}/pay` بدونِ ورود و بدونِ سنجشِ مالکیت | کم | کد |
|
||||
| ۱۹ | دیتابیس و بکاپها برای همهی کاربرانِ ماشینِ مشترک خواندنی بودند | بالا | کد آماده (`deploy.sh`)؛ اعمال با دیپلوی |
|
||||
| ۲۰ | یونیتِ systemd هنوز `DJANGO_DEBUG=1` و کلیدِ ناامن دارد؛ فقط یک خطِ `.env` جلوی DEBUG را گرفته | بالا (نهفته) | ادمینِ سرور |
|
||||
| ۲۱ | `urllib3 2.7.0` و `python-dotenv 1.0.1` آسیبپذیریِ شناختهشده داشتند | کم | کد آماده؛ اعمال با دیپلوی |
|
||||
| ۲۲ | سایتِ فرانت هیچ هدرِ امنیتی ندارد (CSP، frame-ancestors، HSTS) | کم | nginx/فرانت |
|
||||
| ۲۳ | سقفِ دستیار برای مهمانها بر اساسِ IP مشترک است؛ کلاسِ پشتِ یک IP بعد از ۲۰ سوال در ساعت ۴۲۹ میگیرد | متوسط (دسترسپذیری) | تصمیمِ محصولی |
|
||||
|
||||
---
|
||||
|
||||
@@ -138,6 +144,12 @@
|
||||
- **رفع (ادمینِ سرور):** پورتِ ۸۰/۴۴۳ فقط برای رنجهای ArvanCloud، یا `set_real_ip_from` +
|
||||
`real_ip_header` در nginx.
|
||||
|
||||
- **بازتولید شد ۲۰۲۶-۱۰-۰۴:** `curl -H 'Host: mathapi.tadrisino2.ir' -H 'X-Forwarded-For: 1.2.3.4'
|
||||
http://78.38.35.249/api/health` جواب داد `clientIp: 1.2.3.4`. یعنی هر سقفِ IP-محور (ورودِ ادمین
|
||||
۱۰ در ۱۵ دقیقه، درخواست و ورودِ OTP، ساختِ مهمان، دستیارِ مهمان) با عوض کردنِ این هدر در هر
|
||||
درخواست دور زده میشود، و حفاظتِ DDoS/WAFِ آروان هم برای این مسیر وجود ندارد. سقفهای
|
||||
per-phone و per-code که در دیتابیس شمرده میشوند متأثر نیستند. پورتهای ۸۰ و ۴۴۳ هر دو باز بودند.
|
||||
|
||||
## ۹. نبودِ کشِ مشترک (`CACHE_URL`)
|
||||
|
||||
- **چیست:** بدونِ `CACHE_URL` هر worker کشِ `LocMemCache` خودش را دارد (۳ worker).
|
||||
@@ -204,6 +216,8 @@
|
||||
`docker` و `lxd`، روی ماشینی که Gitea و ایمیلِ شرکت هم رویش است. رمز یک بار در گفتگو با
|
||||
agent گذاشته شده و روی دسکتاپِ صاحبِ پروژه در یک فایلِ متنی نگه داشته میشود.
|
||||
- **خطر:** هر نشتِ رمز یعنی دسترسیِ root به کلِ ماشینِ شرکت، نه فقط این پروژه.
|
||||
۲۰۲۶-۱۰-۰۴: `PasswordAuthentication yes` در `sshd_config` و پورتِ ۲۲ از اینترنت باز است؛ پس رمز
|
||||
از هر جای اینترنت حدسزدنی است (fail2ban سنجیده نشد).
|
||||
- **رفع (ادمینِ سرور):** کلیدِ SSH بهجای رمز، تعویضِ رمز، حسابِ جدا برای این پروژه با
|
||||
دسترسیِ فقط به `~/mathapp-v2` و یک خطِ `NOPASSWD` فقط برای
|
||||
`systemctl restart zareian-mathapp`؛ پاک کردنِ فایلِ رمز از دسکتاپ (Keychain جایگزین است).
|
||||
@@ -252,6 +266,63 @@
|
||||
- **رفع:** در `paymentUrl` یک توکنِ امضاشدهی کوتاهعمر (`signing`) با `order_id` بگذارید و
|
||||
همانجا بسنجید؛ یا شناسهی غیرترتیبی.
|
||||
|
||||
## ۱۹. دیتابیس و بکاپها برای همهی کاربرانِ ماشین خواندنی بودند (۲۰۲۶-۱۰-۰۴)
|
||||
|
||||
- **چیست:** `/home/administrator` با مجوزِ 755، `~/mathapp-v2` و `.deploy-backups` با 775، و
|
||||
`db.sqlite3` و هر ۳۵ بکاپ با 644. `.env` درست 600 بود.
|
||||
- **بازتولید شد:** `sudo -u nobody head -c 15 ~/mathapp-v2/db.sqlite3` و همین برای یک بکاپ، هر دو
|
||||
`SQLite format 3` چاپ کردند. هر حسابِ دیگرِ این ماشین (aria، artina، behdad، zahra، git، gitea،
|
||||
metabase) یا هر سرویسِ نفوذشدهی دیگری شمارهها، گفتگوهای کودکان و refresh tokenهای بیهش (مورد ۱۱،
|
||||
یعنی ورود بهجای هر کاربر) را میخواند.
|
||||
- **رفع در کد:** مرحلهی «مجوزِ فایلهای داده» در `deploy.sh`: پوشهی پروژه بیدسترسیِ others،
|
||||
دیتابیس 600، پوشهی بکاپ 700 و فایلهایش 600، در هر انتشار (بکاپِ دستیِ تازه دوباره 644 ساخته
|
||||
میشود). خودِ `/home/administrator` دست نخورد چون nginx فرانت را از `~/mathapp_frontend_new/dist`
|
||||
میخواند. بعد از دیپلوی باید دوباره با `sudo -u nobody` سنجیده شود. هش کردنِ refresh token (مورد ۱۱)
|
||||
هنوز باز است.
|
||||
|
||||
## ۲۰. یونیتِ systemd: DEBUG و کلیدِ ناامن (۲۰۲۶-۱۰-۰۴)
|
||||
|
||||
- **چیست:** `systemctl cat gunicorn-mathapp` هنوز `Environment="DJANGO_DEBUG=1"` و
|
||||
`Environment="DJANGO_SECRET_KEY=django-insecure-…"` دارد. `EnvironmentFile=.env` بعد از آن میآید و
|
||||
`.env` خطِ `DJANGO_DEBUG=0` دارد (شمرده شد، مقدار چاپ نشد)؛ برای همین الان DEBUG خاموش است.
|
||||
- **خطر:** اگر روزی آن یک خط از `.env` پاک شود (مثلاً با این فکر که «پیشفرضِ DEBUG خاموش است»)،
|
||||
بعد از ریاستارت DEBUG روشن میشود و کدِ `0000` برای هر شمارهای پذیرفته میشود. `deploy.sh` این را
|
||||
نمیبیند، چون تنظیماتِ مؤثر را با محیطِ شِل میخواند نه محیطِ یونیت.
|
||||
- **رفع (ادمینِ سرور، با بکاپِ یونیت):** هر دو خطِ `Environment=` را از یونیت بردارید، `daemon-reload`،
|
||||
ریاستارت. حذفِ خطِ کلید بهتنهایی کسی را بیرون نمیاندازد اگر `.env` همان کلید را داشته باشد؛
|
||||
عوض کردنِ خودِ کلید (مورد ۱) جداست.
|
||||
|
||||
## ۲۱. کتابخانههای آسیبپذیر (۲۰۲۶-۱۰-۰۴)
|
||||
|
||||
- **چیست:** پایگاهِ OSV برای ۴۸ بستهی نصبشده: `urllib3 2.7.0` (GHSA-8988-9cw3-xx77 و
|
||||
GHSA-vxq7-64xx-v4gw با شدتِ بالا، GHSA-gh4c-6fx4-qh6g متوسط) و `python-dotenv 1.0.1`
|
||||
(GHSA-mf9w-mj56-hr94، فقط در `set_key` که پروژه صدا نمیزند). اثرِ عملی کم است: سناریوی هر سهی
|
||||
urllib3 پاسخِ مخربِ سرورِ بالادستی یا پراکسی است (بازار، زرینپال، کاوهنگار، Qdrant). `npm audit` روی
|
||||
وابستگیهای اجراییِ فرانت صفر.
|
||||
- **رفع در کد:** `python-dotenv==1.2.2`، `urllib3>=2.8.0`، و `gunicorn==26.2.0` (تا امروز فقط دستی
|
||||
روی سرور نصب بود). هر سه با پایتونِ ۳.۱۰ِ سرور سازگارند و سرور به PyPI دسترسی دارد (سنجیده شد).
|
||||
|
||||
## ۲۲. هدرهای امنیتیِ فرانت (۲۰۲۶-۱۰-۰۴)
|
||||
|
||||
- **چیست:** `https://math.tadrisino2.ir/` فقط `x-xss-protection` دارد؛ نه CSP، نه `X-Frame-Options`/
|
||||
`frame-ancestors`، نه HSTS، نه `X-Content-Type-Options`. API همهی اینها را جز HSTS و CSP دارد.
|
||||
- **خطر:** صفحهی اپ را میشود در iframeِ سایتِ دیگری گذاشت (clickjacking)؛ و چون توکن در
|
||||
`localStorage` است (مورد ۱۶)، نبودِ CSP اثرِ هر XSSِ احتمالی را کامل میکند. در خودِ فرانت سینکِ XSS
|
||||
پیدا نشد (تنها `dangerouslySetInnerHTML` روی SVGِ ثابتِ شکلکهاست).
|
||||
- **رفع:** در nginxِ `mathapp-front` یا در آروان: `Content-Security-Policy` با `frame-ancestors 'none'`
|
||||
و `connect-src` به دامنهی API، و `X-Content-Type-Options: nosniff`.
|
||||
|
||||
## ۲۳. سقفِ دستیار برای مهمانها روی IPِ مشترک (۲۰۲۶-۱۰-۰۴)
|
||||
|
||||
- **چیست:** `GuestAwareAIThrottle` برای مهمانها سطلِ `ai_ask` (۲۰ در ساعت) را بر اساسِ IP میسازد تا
|
||||
ساختنِ مهمانِ تازه سقف را دور نزند.
|
||||
- **بازتولید شد:** ۲۲ مهمانِ متفاوت از یک IP، هر کدام اولین سوالش: ۲۰ تا ۲۰۰، ۲۱اُم و ۲۲اُم ۴۲۹.
|
||||
دانشآموزانِ مهمانِ یک کلاس پشتِ وایفایِ مدرسه یا CGNAT با هم ساعتی ۲۰ سوال دارند (روی سرور با ۳
|
||||
workerِ بیRedis تا حدودِ ۶۰).
|
||||
- **چرا تصمیمِ محصولی است:** سقفِ IP جلوی «مهمان بساز، سه سوالِ رایگان بگیر» را میگیرد. از
|
||||
۲۰۲۶-۱۰-۰۳ سقفِ سراسریِ `AI_GLOBAL_DAILY_MAX` (۳۰۰۰ در روز، حدودِ ۶ دلار) هزینه را جدا محدود
|
||||
میکند، پس میشود سطل را per-user کرد و فقط ساختِ مهمان را محدود نگه داشت. تا تصمیم، دست نخورد.
|
||||
|
||||
---
|
||||
|
||||
## چیزهایی که بسته شدهاند (برای اینکه دوباره باز نشوند)
|
||||
|
||||
Reference in New Issue
Block a user