Files
MathApp/docs/security-before-production.md
Zareian 645baf846b Signed payment links, hashed refresh tokens, login-only leaderboard, load test tools
- /api/payment/orders/{id}/pay requires the short-lived signature that paymentUrl carries;
  without it (or with another order's) it is the same 404 as an unknown order
- Session.refresh_token stores the SHA-256 of the token; migration accounts.0007 hashes the
  existing rows so tokens clients already hold keep working
- /api/leaderboard requires a token (guests have one); names of children were scrapable
- RAG mock only for `manage.py test`, not any command with an argument named "test"
- tools/loadtest: user factory, load generator, fake GapGPT+Qdrant, with method notes
- security-before-production.md lists only open items; closed ones become a guards table
  with the tests that hold them. Comments describe current behaviour, not history

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 21:12:28 +03:30

24 KiB
Raw Blame History

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

آخرین بازبینی با کد و سرورِ زنده: ۲۰۲۶-۱۰-۰۴ (۱۲ مهر ۱۴۰۵).

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

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

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


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

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

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

  • چیست: /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ِ خام.

۱۱. کدِ OTP بی‌هش در دیتابیس

  • چیست: OTPCode.code متنِ ساده است و در فهرستِ پنلِ ادمین (OTPCodeAdmin) دیده می‌شود. توکنِ تمدید (Session.refresh_token) هش‌شده است و این‌جا شامل نیست.
  • چرا باز مانده: تیم برای آزمایش بی‌پیامک یک ردیفِ OTPCode در پنل می‌سازد و کدش را می‌بیند.
  • خطر: کسی که پنل یا دیتابیس را ببیند، در چند دقیقه‌ی عمرِ هر کد می‌تواند با آن وارد شود.
  • رفع: ذخیره‌ی هش و حذفِ ستون از فهرستِ پنل، همراه با راهِ دیگری برای ورودِ آزمایشی.

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

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

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

  • چیست: 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.

۲۰. یونیتِ 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 همان کلید را داشته باشد؛ عوض کردنِ خودِ کلید (مورد ۱) جداست.

۲۲. هدرهای امنیتیِ فرانت

  • چیست: 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 کرد و فقط ساختِ مهمان را محدود نگه داشت. تا تصمیم، دست نخورد.

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

هر تغییری در این مسیرها باید تست‌های نام‌برده را سبز نگه دارد. جزئیاتِ پرداخت و تخفیف در docs/architecture.md.

حفاظ تست
پلنِ هر خرید روی سفارش؛ خرید اشتراکِ فعال را دست نمی‌زند؛ یک کدِ تخفیف پیش از درگاه؛ امتیاز فقط از جوابِ ثبت‌شده payment/tests_audit_fixes.py، accounts/tests_audit_fixes.py
لینکِ پرداخت (/api/payment/orders/{id}/pay) فقط با امضای کوتاه‌عمرِ همراهِ paymentUrl باز می‌شود payment/tests.py (PaymentRedirectViewTests)
توکنِ تمدید فقط به‌صورتِ هشِ SHA-256 ذخیره می‌شود accounts/tests.py (RefreshTokenStoredHashedTests)
جدولِ رتبه‌بندی فقط برای کاربرِ واردشده progress/tests.py (LeaderboardPeriodValidationTests)
شماره‌ی موبایل نرمال می‌شود؛ cooldown، سقفِ روزانه‌ی هر شماره و سقفِ کلِ پیامک در دیتابیس شمرده می‌شوند accounts/tests_otp_limits.py
OTPِ ۶ رقمی با ۵ حدس برای هر کد؛ خروج از حساب توکن‌های قبلی را باطل می‌کند accounts/tests_audit_fixes.py
قلبِ صفر جوابِ سوال و پایانِ درس/جزیره را می‌بندد؛ چکش داخلِ تراکنش است content/tests_hearts_gate.py
select_for_update مؤثر با transaction_mode: IMMEDIATE؛ مقایسه‌ی زمان‌ثابتِ X-Internal-Key payment/tests_audit_fixes.py
دیتابیس و بکاپ‌ها فقط برای صاحبِ سرویس خواندنی‌اند (مرحله‌ی «مجوزِ فایل‌های داده» در deploy.sh) با sudo -u nobody head -c 15 ~/mathapp-v2/db.sqlite3 روی سرور باید خطای دسترسی بدهد
کتابخانه‌ها بدونِ آسیب‌پذیریِ شناخته‌شده (urllib3>=2.8.0، python-dotenv 1.2.2) پایگاهِ OSV روی pip freeze