# پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز مانده‌اند **تاریخِ ثبت: ۲۰۲۶-۰۹-۲۵ (۳ مهر ۱۴۰۵)، کامیتِ پایه `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` را سبز نگه دارد.