Files
MathApp/docs/security-before-production.md
Zareian 2cf2d14dd4 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>
2026-10-04 14:39:11 +03:30

32 KiB
Raw Blame History

پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز مانده‌اند

تاریخِ ثبت: ۲۰۲۶-۰۹-۲۵ (۳ مهر ۱۴۰۵)، کامیتِ پایه 66066cb. بازبینیِ دوباره با سرورِ زنده: ۲۰۲۶-۱۰-۰۴ (مواردِ ۱۹ تا ۲۳ و بازتولیدِ مورد ۸).

این پروژه تا این تاریخ در حالتِ دمو و ارائه‌ی کارآموزی است، نه محصولِ نهایی. چند چیز عمداً باز گذاشته شده تا توسعه‌دهنده‌ها، فرانت‌کار و ارائه کار کنند. هر کدام برای یک سرورِ عمومی با کاربرِ واقعی (کودک، پرداختِ واقعی) خطر است و باید پیش از آمدنِ اولین کاربرِ واقعی بسته شود.

برای agentها و توسعه‌دهنده‌های بعدی: این فهرست ادعاست، نه ضمانت؛ ممکن است از این تاریخ به بعد چیزی عوض شده باشد. پیش از بستنِ هر مورد، وضعیتِ فعلی‌اش را با خودِ سرور یا کد بسنجید (ستونِ «چطور بسنجید»). بعد از بستنِ هر مورد، همین‌جا تاریخ و کامیتِ رفعش را بنویسید؛ مورد را پاک نکنید.

هر آزمونِ «از بیرون» با curl بدونِ توکن انجام شده و در تاریخِ بالا تأیید شده است.


خلاصه، به ترتیبِ خطر

# مورد خطر کارِ کیست
۱ کلیدِ امضای توکن روی سرور همان رشته‌ی داخلِ ریپو است بحرانی — جعلِ ورود به‌جای هر کاربر کد آماده؛ اعمال روی سرور
۲ ۷ حسابِ دمو با کدِ ورودِ ثابت روی پروداکشن بالا .env سرور
۳ داده‌ی دمو و حسابِ مهمانِ عمومیِ frontend-test-device بالا دیتابیسِ سرور
۴ /api/health جزئیاتِ داخلی را به همه نشان می‌دهد متوسط کد
۵ Swagger، ReDoc و schemaی کاملِ API عمومی‌اند متوسط کد
۶ جدولِ رتبه‌بندی نام و شناسه‌ی کودکان را بدونِ ورود نشان می‌دهد متوسط (حریمِ خصوصیِ کودک) کد
۷ پنلِ /admin/ روی دامنه‌ی عمومی متوسط nginx + کد
۸ سرور بدونِ ArvanCloud روی پورتِ ۸۰ هم جواب می‌دهد متوسط ادمینِ سرور
۹ کشِ مشترک (Redis) نیست؛ محدودیت‌ها per-worker‌اند متوسط .env سرور
۱۰ ورودِ مهمان فقط با deviceId است کم (طراحی) کد
۱۱ توکنِ refresh و کدِ OTP بی‌هش در دیتابیس کم کد
۱۲ هزینه‌ی دستیارِ AI با ساختِ حسابِ مهمان قابلِ بالا بردن است کم (مالی) کد
۱۳ محدودیتِ قلب در سرور خاموش است (HEARTS_ENFORCED=0) بسته شد ۲۰۲۶-۱۰-۰۲ کم (کسب‌وکار) .env + فرانت
۱۴ SQLite روی دیسکِ مشترکِ پُر، بدونِ بکاپِ خودکارِ بیرون از ماشین کم (داده) زیرساخت
۱۵ دسترسیِ سرور: حسابِ مشترک، رمز، sudoِ کامل بالا ادمینِ سرور
۱۶ فرانت توکن را در 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 بعد از ۲۰ سوال در ساعت ۴۲۹ می‌گیرد متوسط (دسترس‌پذیری) تصمیمِ محصولی

۱. کلیدِ امضای توکن (DJANGO_SECRET_KEY) روی سرور

  • چیست: توکنِ ورود فقط user_idِ امضاشده با django.core.signing است (accounts/authentication.py). امضا با DJANGO_SECRET_KEY انجام می‌شود. روی سرور این مقدار هنوز همان fallbackِ django-insecure-… است که در config/settings.py کامیت شده (آخرین تأیید: ۲۰۲۶-۰۹-۲۴، به‌صورتِ بولین).
  • خطر: هر کس ریپو را ببیند (Gitea، zipِ تحویلی، هر کپی) می‌تواند برای هر user_id توکنِ معتبر بسازد و به‌جای آن کاربر وارد شود؛ از جمله خریدِ اشتراک، دیدنِ گفتگوها و داده‌ی خانواده.
  • چرا باز مانده: عوض کردنش همه‌ی کاربرهای واردشده (توسعه‌دهنده‌ها، فرانت‌کار، حساب‌های دمو در حالِ ارائه) را بیرون می‌اندازد. تصمیمِ صاحبِ پروژه: بعد از ارائه.
  • رفع: DJANGO_SECRET_KEY را از .env و از یونیتِ systemd حذف کنید تا config/settings.py یک کلیدِ تصادفی بسازد و در .django_secret_key (مجوز 0600) نگه دارد. سرویس را ری‌استارت کنید. همه یک بار دوباره وارد می‌شوند.
  • چطور بسنجید: manage.py check --deploy نباید security.W009 بدهد؛ روی سرور venv/bin/python -c "from django.conf import settings; print(settings.SECRET_KEY.startswith('django-insecure'))" باید False چاپ کند (خودِ کلید را چاپ نکنید).

۲. حساب‌های دمو با کدِ ورودِ ثابت (TEST_LOGIN_PHONES)

  • چیست: ۷ شماره‌ی 09120000101 تا 09120000107 با کدِ ثابت در .env سرور. شماره‌ها در ریپو (docs/operations.md، CLAUDE.md) آمده‌اند؛ کدها فقط در راهنمای ارائه‌ی صاحبِ پروژه‌اند.
  • خطر: کسی که کد را از راهنما، اسکرین‌شات یا ارائه ببیند وارد آن حساب‌ها می‌شود. این کدها با OTP_MAX_FAILED_ATTEMPTS باطل نمی‌شوند (ردیفِ OTPCode ندارند)؛ فقط سقفِ per-workerِ تلاشِ ورود جلویشان است.
  • رفع: TEST_LOGIN_PHONES را از .env سرور پاک کنید و ری‌استارت کنید.
  • چطور بسنجید: /api/health دیگر هشدارِ «شماره‌های تستِ ورود با کدِ ثابت فعال‌اند» ندارد.

۳. داده‌ی دمو و مهمانِ عمومی

  • افزوده ۲۰۲۶-۰۹-۲۵: کاربرِ ۱۷ (09000000001، «کاربر تست»، deviceId = deploy-check-20260925) از تستِ دیپلویِ پیامک، و کاربرِ ۱۸ (مهمان، deploy-check-ai-20260925) از تستِ دستیار؛ همراهِ بقیه پاک شوند.
  • چیست: کاربرهای ۶ (deviceId = frontend-test-device) و ۱۰ تا ۱۵، با درس، گفتگو، تیکت، خانواده و زیرمجموعه، روی دیتابیسِ پروداکشن ساخته شده‌اند. frontend-test-device در ریپو نوشته شده؛ هر کس POST /api/auth/session با همین deviceId بزند، توکنِ همان حساب را می‌گیرد.
  • خطر: داده‌ی ساختگی در آمار و جدولِ رتبه‌بندیِ واقعی؛ یک حسابِ عمومی که هر کس می‌تواند با آن کار کند (ثبتِ تیکت، گفتگو با AI به هزینه‌ی ما).
  • رفع: پیش از کاربرِ واقعی پاک شوند. purge_demo_data این‌ها را نمی‌گیرد (از مسیرِ واقعیِ viewها ساخته شده‌اند)؛ باید با شناسه پاک شوند، پیش از آن بکاپ.
  • چطور بسنجید: POST /api/auth/session با frontend-test-device باید isNewUser: true بدهد.

۴. /api/health عمومی و پرجزئیات

  • چیست: config/health.py، AllowAny. بدونِ توکن برمی‌گرداند: debug، warnings (مثلاً «شماره‌های تست فعال‌اند»، «محدودیتِ نرخ بینِ workerها مشترک نیست»)، integrations (کدام سرویس‌ها پیکربندی‌اند، وضعیتِ اعتبارِ AI، نامِ مدل)، traffic (تعدادِ کاربر، فعالِ ۲۴ ساعتِ اخیر، زمانِ آخرین فعالیت)، allowedOrigins، clientIp، شمارِ محتوا و مهاجرت‌ها.
  • خطر: نقشه‌ی حمله: مهاجم می‌فهمد محدودیت‌ها سه‌برابر دورزدنی‌اند، حسابِ کدِ ثابت هست، اعتبارِ AI کی تمام شده، و ترافیکِ واقعی چقدر است.
  • چرا باز مانده: صاحبِ پروژه و agentها بدونِ دسترسیِ SSH از همین صفحه سلامتِ سرور را می‌بینند.
  • رفع: پاسخِ عمومی فقط {"status": "ok"|"degraded"}؛ بقیه پشتِ هدرِ X-Internal-Key (INTERNAL_SERVICE_KEY) یا فقط برای staff.
  • چطور بسنجید: curl https://mathapi.tadrisino2.ir/api/health بدونِ هدر نباید warnings و traffic داشته باشد.

۵. مستنداتِ API عمومی

  • چیست: /api/docs/ (Swagger)، /api/redoc/ و /api/schema/ بدونِ ورود باز‌اند (تأیید: HTTP 200). schema همه‌ی endpointها، پارامترها، کدهای خطا و هدرهای داخلی (X-Internal-Key) را فهرست می‌کند.
  • خطر: شناساییِ کاملِ سطحِ حمله بدونِ زحمت.
  • چرا باز مانده: قراردادِ مشترک با فرانت همین‌جاست؛ فرانت‌کار و ارائه به آن نیاز دارند.
  • رفع: در پروداکشن SERVE_PERMISSIONS در SPECTACULAR_SETTINGS را به IsAdminUser (staffِ جنگو) محدود کنید، یا مسیر را در nginx به IPهای تیم ببندید.
  • چطور بسنجید: curl -o /dev/null -w "%{http_code}" …/api/schema/ باید ۴۰۱/۴۰۳ بدهد.

۶. جدولِ رتبه‌بندیِ عمومی — حریمِ خصوصیِ کودک

  • چیست: GET /api/leaderboard بدونِ توکن (تأیید: HTTP 200) برای هر ردیف rank، userId، name و xpInPeriod می‌دهد.
  • خطر: نامِ کودکان و شناسه‌ی داخلی‌شان برای همه‌ی اینترنت قابلِ جمع‌آوری است. پاک‌سازیِ نام (حذفِ شماره و لینک، clean_display_name) فقط راهِ تماس را می‌گیرد، نه خودِ نام را.
  • رفع: IsAuthenticated؛ حذفِ userId از پاسخ (جز برای ردیفِ خودِ کاربر)؛ نمایشِ نامِ کوچک یا نامِ مستعار.

۷. پنلِ ادمین روی دامنه‌ی عمومی

  • چیست: /admin/ روی mathapi.tadrisino2.ir، با ورودِ نام‌کاربری/رمزِ جنگو. از ۲۰۲۶-۰۹-۲۵ سقفِ ۱۰ تلاش در ۱۵ دقیقه برای هر IP دارد (AdminLoginThrottleMiddleware)، ولی: شمارش per-worker است (مورد ۹)، IP با XFF جعل‌پذیر است (مورد ۸)، و ۲FA ندارد.
  • خطر: ادمین همه‌ی محتوا، پلن‌ها، کدهای تخفیف، سفارش‌ها و کاربرها را کنترل می‌کند.
  • رفع: محدود کردنِ /admin/ در nginx به IPهای تیم یا VPN؛ تغییرِ مسیرِ پیش‌فرض؛ ۲FA؛ رمزهای قوی و حسابِ staffِ جدا برای هر نفر.

۸. دسترسیِ مستقیم به پورتِ ۸۰ (دور زدنِ ArvanCloud)

  • چیست: http://78.38.35.249/api/health با Host: mathapi.tadrisino2.ir پاسخ می‌دهد (تأیید: HTTP 200). NUM_PROXIES=2 فرض می‌کند همیشه دو پروکسی جلوی ماست.

  • خطر: کسی که IP را بداند X-Forwarded-For را جعل می‌کند و محدودیت‌های IP-محور (مهمان، OTP، ادمین) را دور می‌زند.

  • رفع (ادمینِ سرور): پورتِ ۸۰/۴۴۳ فقط برای رنج‌های 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).
  • خطر: همه‌ی محدودیت‌های مبتنی بر کش تا سه برابر شل‌ترند: throttleهای DRF، cooldown و سقفِ درخواستِ OTP، سقفِ تلاشِ ورودِ OTP به‌ازای شماره، سقفِ ورودِ ادمین؛ و باطل‌شدنِ کش بعد از تغییرِ ادمین فقط در یک worker. (سقفِ حدسِ هر کدِ OTP در دیتابیس است و متأثر نیست.)
  • رفع: CACHE_URL=redis://127.0.0.1:6379/1 (db0 مالِ اپِ دیگری است) و ری‌استارت.
  • چطور بسنجید: /api/health → checks.cache.backend نباید LocMemCache باشد.

۱۰. ورودِ مهمان فقط با deviceId

  • چیست: POST /api/auth/session با هر deviceId یا حسابِ مهمانِ همان دستگاه را برمی‌گرداند یا می‌سازد (accounts/views.py::GuestSessionView). هیچ رازِ دیگری در کار نیست.
  • خطر: هر کس deviceIdِ یک مهمان را بداند (از localStorage، لاگ، اسکرین‌شات) وارد حسابِ او می‌شود. بعد از ثبت‌نام با شماره این راه بسته می‌شود (مهمان دیگر مهمان نیست).
  • رفع: برای مهمان هم یک رازِ تصادفیِ سمتِ سرور صادر شود و ورودِ دوباره با همان راز باشد، نه با deviceIdِ خام.

۱۱. داده‌ی حساس بی‌هش در دیتابیس

  • چیست: Session.refresh_token و OTPCode.code به‌صورتِ متنِ ساده ذخیره می‌شوند (accounts/models.py) و در پنلِ ادمین (Session, OTPCode) دیده می‌شوند.
  • خطر: هر کس بکاپِ دیتابیس یا پنلِ ادمین را ببیند نشستِ هر کاربر را می‌رباید. بکاپ‌ها در .deploy-backups/ کنارِ خودِ پروژه روی یک ماشینِ مشترک‌اند.
  • رفع: ذخیره‌ی هشِ SHA-256 و مقایسه با هش؛ حذفِ این دو فیلد از نمایشِ ادمین.

۱۲. هزینه‌ی دستیارِ AI

  • چیست: از ۲۰۲۶-۱۰-۰۲ هر کاربر روزی ۳ سؤالِ رایگان دارد (AI_DAILY_QUOTA_FREE؛ پیش‌تر ۱۵) و بعد فقط با اعتبارِ خریده‌شده؛ ۲۰ در ساعت. مهمان‌ها بر اساسِ IP محدود می‌شوند ولی هر IP ساعتی ۲۰ حسابِ مهمانِ تازه می‌سازد (guest_session)، و هر حسابِ تازه ۳ سؤالِ رایگانِ تازه دارد. خطر کمتر شد (۳ به‌جای ۱۵)، ولی بسته نیست. طولِ سؤال از ۲۰۲۶-۰۹-۲۵ حداکثر ۲۰۰۰ نویسه است.
  • خطر: با چند IP، اعتبارِ GapGPT در چند ساعت تمام می‌شود (اعتبار یک بار در ۲۰۲۶-۰۹-۲۵ به صفر رسید، البته به دلیلِ دیگر).
  • رفع: سقفِ سراسریِ روزانه برای کلِ اپ؛ دستیار فقط برای کاربرِ واردشده با شماره یا با سقفِ بسیار کمتر برای مهمان؛ هشدار وقتی aiProvider.state در /api/health برابرِ quota_exhausted شد.

۱۳. محدودیتِ قلب خاموش است — بسته شد ۲۰۲۶-۱۰-۰۲

  • چیست: با صفر قلب، سرور همچنان جواب را می‌پذیرد، مگر HEARTS_ENFORCED=1 (content/views.py::QuestionAnswerView). «قلبِ نامحدود» تنها مزیتِ اشتراک است.
  • خطر: مزیتِ پولی فقط در ظاهرِ اپ اعمال می‌شود؛ با صدا زدنِ مستقیمِ API دور زده می‌شود.
  • چرا باز مانده: فرانت برای ۴۰۳ِ no_hearts هنوز صفحه ندارد؛ روشن کردنش دانش‌آموز را به صفحه‌ی خطا می‌برد. رفتار با صفر قلب هم تصمیمِ محصولیِ باز است.
  • رفع: بعد از صفحه‌ی «قلب‌ها تمام شد» در فرانت، HEARTS_ENFORCED=1 در .env.
  • بسته شد ۲۰۲۶-۱۰-۰۲ (بک‌اند 4700ccb، فرانت 33f5f8d): HEARTS_ENFORCED پیش‌فرض روشن؛ چکِ قلب داخلِ تراکنش؛ پایانِ درس/جزیره‌ی بی‌جواب با قلبِ صفر هم بسته؛ صفحه‌ی «قلب‌هات تموم شد» با شمارشِ معکوس. تست‌ها: content/tests_hearts_gate.py.

۱۴. دیتابیس و بکاپ

  • چیست: SQLite در ~/mathapp-v2/db.sqlite3، روی دیسکی که بینِ چند سرویس مشترک است و ~۸۳٪ پر است. transaction_mode: IMMEDIATE هم‌زمانی را درست ولی سریالی می‌کند. بکاپ دستی است (deploy.sh پیش از migrate بکاپ نمی‌گیرد) و در همان پوشه می‌ماند.
  • خطر: از دست رفتنِ کلِ داده با یک خطای دیسک یا یک دستورِ اشتباه؛ پُر شدنِ دیسک سرویس‌های دیگر (Gitea، ایمیل) را هم می‌خواباند.
  • رفع: مهاجرت به Postgres (روی همان ماشین نصب است)، بکاپِ خودکار پیش از هر migrate، و نگه‌داریِ بکاپ بیرون از ماشین.

۱۵. دسترسیِ سرور

  • چیست: ورود با رمز به حسابِ مشترکِ administrator با sudo (ALL : ALL) ALL و عضوِ docker و lxd، روی ماشینی که Gitea و ایمیلِ شرکت هم رویش است. رمز یک بار در گفتگو با agent گذاشته شده و روی دسکتاپِ صاحبِ پروژه در یک فایلِ متنی نگه داشته می‌شود.
  • خطر: هر نشتِ رمز یعنی دسترسیِ root به کلِ ماشینِ شرکت، نه فقط این پروژه. ۲۰۲۶-۱۰-۰۴: PasswordAuthentication yes در sshd_config و پورتِ ۲۲ از اینترنت باز است؛ پس رمز از هر جای اینترنت حدس‌زدنی است (fail2ban سنجیده نشد).
  • رفع (ادمینِ سرور): کلیدِ SSH به‌جای رمز، تعویضِ رمز، حسابِ جدا برای این پروژه با دسترسیِ فقط به ~/mathapp-v2 و یک خطِ NOPASSWD فقط برای systemctl restart zareian-mathapp؛ پاک کردنِ فایلِ رمز از دسکتاپ (Keychain جایگزین است).

۱۶. نگه‌داریِ توکن در فرانت

  • چیست: کلاینتِ فرانت (src/api/index.ts در مخزنِ فرانت) توکن و refresh را در localStorage نگه می‌دارد.
  • خطر: هر XSS در سایت هر دو را می‌خواند.
  • رفع: CSPِ سخت‌گیرانه روی math.tadrisino2.ir؛ در بلندمدت refresh در کوکیِ HttpOnly.

۱۷. شماره‌ی موبایل نرمال نمی‌شود (افزوده در بازبینیِ دومِ ۲۰۲۶-۰۹-۲۵)

  • چیست: phone در ورود، درخواستِ OTP و دعوتِ خانواده رشته‌ی خام است (accounts/serializers.py، max_length=15 بدونِ اعتبارسنجی). 09121234567، +989121234567، 989121234567 و همان شماره با ارقامِ فارسی چهار «شماره»ی جدا‌اند.
  • خطر: cooldownِ ۲ دقیقه‌ای و سقفِ ۵ در ساعتِ هر شماره به‌ازای هر املا جداست، پس با چند IP می‌شود یک گوشی را با پیامک بمباران کرد (وقتی کاوه‌نگار وصل شود، به هزینه‌ی ما)؛ یک آدم با دو املا دو حسابِ جدا می‌سازد؛ دعوتِ والد با املای دیگر ۴۰۳ می‌گرفت (بخشِ والدین ۲۰۲۶-۱۰-۰۱ حذف شد). (بازتولید شد با تستِ موقت؛ پیامکِ واقعی فرستاده نشد.)
  • رفع: یک تابعِ normalize_phone (ارقامِ فارسی/عربی → لاتین، +98/0098/98 → 0، فقط 09\d{9}) در هر سه serializer، و migrationِ داده برای ردیف‌های موجود.
  • چطور بسنجید: دو درخواستِ OTP پشتِ‌سرِهم با 0912… و +98912… باید دومی ۴۲۹ بگیرد.
  • ✅ بسته شد ۲۰۲۶-۰۹-۲۵ (همان روزی که کلیدِ کاوه‌نگار روی سرور رفت؛ کامیت 0580aec، روی سرور از ۱۲:۳۲). accounts/phone.py::normalize_phone در ورود، درخواستِ OTP، دعوتِ خانواده و کلیدهای TEST_LOGIN_PHONES؛ migrationِ accounts.0005 شماره‌های موجود را نرمال می‌کند (ردیفی که شکلِ نرمالش مالِ کاربرِ دیگری است دست نمی‌خورد و فقط شمرده می‌شود). parentPhone در پروفایل هنوز نرمال نمی‌شود. همراهش، چون از این تاریخ پیامکِ واقعی خرجِ اعتبار است:
    • cooldownِ ۲ دقیقه‌ای و سقفِ روزانه‌ی هر شماره (OTP_REQUEST_PHONE_DAILY_MAX=10) حالا در دیتابیس هم شمرده می‌شوند، پس روی ۳ workerِ سرور سه‌برابر نمی‌شوند (بندِ ۹ برای این دو دیگر صدق نمی‌کند؛ برای throttleهای IP هنوز می‌کند).
    • سقفِ روزانه‌ی کلِ پیامکِ OTP (OTP_SMS_GLOBAL_DAILY_MAX=300) جلوی SMS pumping؛ رسیدن به آن ۵۰۳ و لاگِ ERROR می‌دهد. با رشدِ کاربرِ واقعی از .env بالا ببرید.
    • سقف‌های per-IP گشاده‌تر شدند (کد ۲۰، ورود ۳۰، مهمان ۶۰ در ساعت، همه از .env قابلِ تغییر) چون پشتِ CGNAT و وای‌فایِ مدرسه ده‌ها نفر یک IP دارند.
    • تست‌ها: accounts/tests_otp_limits.py. هم‌زمانیِ واقعیِ دو پروسه‌ی gunicorn تست نشده.

۱۸. صفحه‌ی پرداخت مالکیت را نمی‌سنجد (افزوده در بازبینیِ دومِ ۲۰۲۶-۰۹-۲۵)

  • چیست: PaymentRedirectView (AllowAny) برای هر شناسه‌ی سفارشِ ترتیبی از زرین‌پال authority می‌گیرد.
  • خطر: غریبه با پیمایشِ /orders/1/pay، /orders/2/pay… روی سفارش‌های باز authority می‌نشاند؛ از آن به بعد صاحبِ سفارش «کدِ تخفیف را پیش از درگاه وارد کنید» می‌گیرد (بازتولید شد، با زرین‌پالِ mock). وضعیتِ paid/failedِ سفارشِ دیگران هم از ریدایرکت پیداست.
  • رفع: در 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 کرد و فقط ساختِ مهمان را محدود نگه داشت. تا تصمیم، دست نخورد.

چیزهایی که بسته شده‌اند (برای اینکه دوباره باز نشوند)

در بازبینیِ ۲۰۲۶-۰۹-۲۵ (کامیتِ 42783f9) بسته شدند؛ جزئیات در docs/architecture.md: پلنِ هر خرید روی سفارش، دست نخوردنِ اشتراکِ فعال هنگامِ خرید، یک کدِ تخفیف پیش از درگاه، حذفِ /api/payment/webhook، امتیاز فقط از جوابِ ثبت‌شده، OTPِ ۶ رقمی با ۵ حدس، خروج از حساب، حفظِ مهمان هنگامِ ثبت‌نام، select_for_update مؤثر با IMMEDIATE، سقفِ ورودِ ادمین، سقفِ طولِ سؤالِ AI، مقایسه‌ی زمان‌ثابتِ X-Internal-Key.

هر تغییری در این مسیرها باید تست‌های payment/tests_audit_fixes.py و accounts/tests_audit_fixes.py را سبز نگه دارد.