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:
@@ -50,6 +50,7 @@
|
||||
| ۲۰۲۶-۰۹-۲۶ | میانگینِ همهی جوابهای سنجش | `gpt-4.1-mini` | ۰٫۰۹۶ سنت | میانگینِ `costUsd`ِ ۵۴۱ فراخوانی (۱) |
|
||||
| ۲۰۲۶-۱۰-۰۱ | یک جوابِ دستیار، پرامپتِ `tutor-v11-math-only`، ۱۰۳ مسئلهی کتاب | `gpt-4.1-mini` | ۰٫۱۶۲ سنت (دبستان ۰٫۱۲۹، دبیرستان ۰٫۲۲۵) | توکنِ ورودی تخمینی (۰٫۳۸۷ توکن به ازای هر حرف)، خروجی از جوابِ واقعی، ضربدر قیمتِ بالا |
|
||||
| ۲۰۲۶-۱۰-۰۱ | یک سوالِ زنده در اپ (۱۲ سوال) | `gpt-4.1-mini` | ۰٫۲۲۵ سنت | فراخوانیِ واقعی |
|
||||
| ۲۰۲۶-۱۰-۰۴ | یک جوابِ دستیار در لودتستِ سرورِ زنده (۵۰ جواب، استریم، سوالهای تصادفیِ بانکِ سوالِ ده پایه) | `gpt-4.1-mini` | ۰٫۱۸۳ سنت (میانگین: ۳٬۱۸۱ توکنِ ورودی، ۳۴۸ خروجی) | `prompt_tokens`/`completion_tokens`ِ ذخیرهشده روی پیامها ضربدر قیمتِ بالا؛ داشبوردِ GapGPT خوانده نشد |
|
||||
|
||||
عددِ برنامهریزیِ قیمتگذاری **۰٫۲۰ سنت برای هر سوالِ دستیار** است و حدودِ ۷۰٪ِ آن ورودی است.
|
||||
هزینهی embedding جدا اندازه گرفته نشده است. قیمتِ رسمیِ `text-embedding-3-small` بسیار کمتر از
|
||||
|
||||
@@ -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 کرد و فقط ساختِ مهمان را محدود نگه داشت. تا تصمیم، دست نخورد.
|
||||
|
||||
---
|
||||
|
||||
## چیزهایی که بسته شدهاند (برای اینکه دوباره باز نشوند)
|
||||
|
||||
@@ -932,7 +932,7 @@ fetch ← `npm ci` ← بررسیِ نوعها ← build در `dist.next` ←
|
||||
|
||||
### ۱۴-۳ مقیاسِ اجرا: کجا گلوگاه است و بعد چه
|
||||
|
||||
وضعِ فعلی: ۲۴ درخواستِ همزمان؛ درخواستِ عادی چند ده میلیثانیه، جوابِ دستیار دهها ثانیه. توکن stateless است، پس هر تعداد نسخهی دیگر از بکاند پشتِ nginx بیتغییرِ کد کار میکند — به شرطِ دیتابیس و کشِ مشترک.
|
||||
وضعِ فعلی: ۳ worker × ۳۲ نخ = ۹۶ درخواستِ همزمان (تا ۲۰۲۶-۱۰-۰۴ ۸ نخ بود)؛ درخواستِ عادی چند ده میلیثانیه، جوابِ دستیار روی سرور میانهی ۵ تا ۸ ثانیه و گاهی ۴۰ ثانیه، که تمامش یک نخ را نگه میدارد. لودتستِ همان روز: با ۸ نخ از حدودِ ۲۰ پرسشِ همزمان صفحههای عادی چند ثانیه صف کشیدند؛ با ۳۲ نخ تا ۷۵ پرسش نه. توکن stateless است، پس هر تعداد نسخهی دیگر از بکاند پشتِ nginx بیتغییرِ کد کار میکند — به شرطِ دیتابیس و کشِ مشترک.
|
||||
|
||||
| # | گلوگاه | راهحلِ سادهی بعدی | نیاز |
|
||||
|---|---|---|---|
|
||||
|
||||
Reference in New Issue
Block a user