- /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>
24 KiB
پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز ماندهاند
آخرین بازبینی با کد و سرورِ زنده: ۲۰۲۶-۱۰-۰۴ (۱۲ مهر ۱۴۰۵).
این پروژه تا این تاریخ در حالتِ دمو و ارائهی کارآموزی است، نه محصولِ نهایی. چند چیز عمداً باز گذاشته شده تا توسعهدهندهها، فرانتکار و ارائه کار کنند. هر کدام برای یک سرورِ عمومی با کاربرِ واقعی (کودک، پرداختِ واقعی) خطر است و باید پیش از آمدنِ اولین کاربرِ واقعی بسته شود.
برای 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 |