docs/security-before-production.md records, item by item, what is exposed on purpose while this is an internship demo: the committed token-signing key still in use on the server, seven fixed-code demo logins, demo users in the production database, a public /api/health that reports internal state, a public API schema, a public leaderboard with children's names, /admin on the public domain, the server answering on port 80 around ArvanCloud, per-worker rate limits without Redis, guest login by deviceId alone, unhashed refresh tokens and OTP codes, AI cost exposure through throwaway guests, hearts not enforced, SQLite without off-box backups, and shared root access to the box. Each has its risk, why it was left, the fix and how to verify it; the exposure checks were run against the live server on 2026-09-25. CLAUDE.md, README and docs/operations.md point to it so no agent mistakes this deployment for production-ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
19 KiB
پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز ماندهاند
تاریخِ ثبت: ۲۰۲۶-۰۹-۲۵ (۳ مهر ۱۴۰۵)، کامیتِ پایه 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 نگه میدارد |
کم | فرانت |
۱. کلیدِ امضای توکن (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دیگر هشدارِ «شمارههای تستِ ورود با کدِ ثابت فعالاند» ندارد.
۳. دادهی دمو و مهمانِ عمومی
- چیست: کاربرهای ۶ (
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://api.math.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/رویapi.math.tadrisino2.ir، با ورودِ نامکاربری/رمزِ جنگو. از ۲۰۲۶-۰۹-۲۵ سقفِ ۱۰ تلاش در ۱۵ دقیقه برای هر IP دارد (AdminLoginThrottleMiddleware)، ولی: شمارش per-worker است (مورد ۹)، IP با XFF جعلپذیر است (مورد ۸)، و ۲FA ندارد. - خطر: ادمین همهی محتوا، پلنها، کدهای تخفیف، سفارشها و کاربرها را کنترل میکند.
- رفع: محدود کردنِ
/admin/در nginx به IPهای تیم یا VPN؛ تغییرِ مسیرِ پیشفرض؛ ۲FA؛ رمزهای قوی و حسابِ staffِ جدا برای هر نفر.
۸. دسترسیِ مستقیم به پورتِ ۸۰ (دور زدنِ ArvanCloud)
- چیست:
http://78.38.35.249/api/healthباHost: api.math.tadrisino2.irپاسخ میدهد (تأیید: HTTP 200).NUM_PROXIES=2فرض میکند همیشه دو پروکسی جلوی ماست. - خطر: کسی که IP را بداند
X-Forwarded-Forرا جعل میکند و محدودیتهای IP-محور (مهمان، OTP، ادمین) را دور میزند. - رفع (ادمینِ سرور): پورتِ ۸۰/۴۴۳ فقط برای رنجهای ArvanCloud، یا
set_real_ip_from+real_ip_headerدر nginx.
۹. نبودِ کشِ مشترک (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
- چیست: سقفِ روزانه ۱۵ سؤال برای هر کاربر (
DAILY_AI_QUOTA) و ۲۰ در ساعت؛ مهمانها بر اساسِ 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.
۱۴. دیتابیس و بکاپ
- چیست: SQLite در
~/mathapp-v2/db.sqlite3، روی دیسکی که بینِ چند سرویس مشترک است و ~۸۳٪ پر است.transaction_mode: IMMEDIATEهمزمانی را درست ولی سریالی میکند. بکاپ دستی است (deploy.shپیش ازmigrateبکاپ نمیگیرد) و در همان پوشه میماند. - خطر: از دست رفتنِ کلِ داده با یک خطای دیسک یا یک دستورِ اشتباه؛ پُر شدنِ دیسک سرویسهای دیگر (Gitea، ایمیل) را هم میخواباند.
- رفع: مهاجرت به Postgres (روی همان ماشین نصب است)، بکاپِ خودکار پیش از هر
migrate، و نگهداریِ بکاپ بیرون از ماشین.
۱۵. دسترسیِ سرور
- چیست: ورود با رمز به حسابِ مشترکِ
administratorباsudo (ALL : ALL) ALLو عضوِdockerوlxd، روی ماشینی که Gitea و ایمیلِ شرکت هم رویش است. رمز یک بار در گفتگو با agent گذاشته شده و روی دسکتاپِ صاحبِ پروژه در یک فایلِ متنی نگه داشته میشود. - خطر: هر نشتِ رمز یعنی دسترسیِ root به کلِ ماشینِ شرکت، نه فقط این پروژه.
- رفع (ادمینِ سرور): کلیدِ SSH بهجای رمز، تعویضِ رمز، حسابِ جدا برای این پروژه با
دسترسیِ فقط به
~/mathapp-v2و یک خطِNOPASSWDفقط برایsystemctl restart zareian-mathapp؛ پاک کردنِ فایلِ رمز از دسکتاپ (Keychain جایگزین است).
۱۶. نگهداریِ توکن در فرانت
- چیست: کلاینتِ فرانت (
src/api/index.tsدر مخزنِ فرانت) توکن و refresh را درlocalStorageنگه میدارد. - خطر: هر XSS در سایت هر دو را میخواند.
- رفع: CSPِ سختگیرانه روی
math.tadrisino2.ir؛ در بلندمدت refresh در کوکیِHttpOnly.
چیزهایی که بسته شدهاند (برای اینکه دوباره باز نشوند)
در بازبینیِ ۲۰۲۶-۰۹-۲۵ (کامیتِ 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 را سبز نگه دارد.