Docs and requirements: signing key replaced and shared Redis cache are live; pin dependency versions

- CLAUDE.md: status block for 2026-10-07 (what changed on the server, how it was checked, what was not).
- docs/security-before-production.md: signing key, shared cache and unit DEBUG items moved to the guard table.
- requirements.txt: ranges replaced by the versions running on the server (pip freeze, 2026-10-07).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 14:02:51 +03:30
parent 4c601087dc
commit 7929f6bd85
3 changed files with 34 additions and 54 deletions

View File

@@ -70,10 +70,10 @@ paid on the server on 2026-10-03); orders without the `question_credits`/`hearts
## Before any real user: read `docs/security-before-production.md`
The server is deliberately left in demo mode for the internship presentation: the token
signing key is the one committed in this repo, seven fixed-code demo logins exist, demo
users sit in the production DB, and `/api/health`, the API schema, the leaderboard and
`/admin/` are all public. Each is listed there with its risk, why it was left, and the
The server is deliberately left in demo mode for the internship presentation: seven
fixed-code demo logins exist, demo users sit in the production DB, and `/api/health`, the API
schema, the leaderboard and `/admin/` are all public. (The token signing key is no longer the
committed one: replaced 2026-10-07, see the status block below.) Each is listed there with its risk, why it was left, and the
fix. **Do not treat this deployment as production-ready until every item there is closed**,
and when one is closed, move it to that file's «حفاظ‌هایی که هست» table with the test that holds it (the date lives in the commit message; the owner wants docs to describe the current state only).
@@ -88,7 +88,7 @@ python manage.py test accounts # single app
python manage.py test accounts.tests.SomeTestCase.test_something # single test
```
Local dev requires `DJANGO_DEBUG=1` (enables Swagger auth via `DEV_MASTER_OTP`/`devOtp` in responses). `DEBUG` defaults to `False`. Production also requires `DJANGO_SECRET_KEY`, `DJANGO_ALLOWED_HOSTS` (comma-separated), and ideally `CACHE_URL` (Redis). Without it the app still boots on per-process `LocMemCache` and `/api/health` warns — throttles and admin-driven cache invalidation are then per worker. On the server it is still unset (left to the server admin, 2026-09-24).
Local dev requires `DJANGO_DEBUG=1` (enables Swagger auth via `DEV_MASTER_OTP`/`devOtp` in responses). `DEBUG` defaults to `False`. Production also requires `DJANGO_SECRET_KEY`, `DJANGO_ALLOWED_HOSTS` (comma-separated), and ideally `CACHE_URL` (Redis). Without it the app still boots on per-process `LocMemCache` and `/api/health` warns — throttles and admin-driven cache invalidation are then per worker. On the server it points at the shared Redis, database 1 (since 2026-10-07).
No pytest; uses Django's built-in test runner. No linter/formatter configured.
@@ -441,6 +441,25 @@ preferred setup, because there is then no key to leak. **Never let the server ru
`django-insecure-…` fallback from `config/settings.py`: that string is committed here, so
anyone who can read this repo can forge a token for any user.**
## وضعیت ۲۰۲۶-۱۰-۰۷ — روی سرور (`4c60108`، ری‌استارت ۱۳:۵۹)
بکاپ‌های همان روز در `.deploy-backups/`: `env-before-secretkey-redis-20261007-135850`،
`unit-gunicorn-mathapp-before-20261007-135850.service`، `db-before-deploy-20261007-13*.sqlite3`.
- **کلیدِ امضای توکن عوض شد** (بند ۱ سندِ امنیتی). از `.env` و از یونیتِ `gunicorn-mathapp.service`
برداشته شد (یونیت فقط `DJANGO_ALLOWED_HOSTS` و `EnvironmentFile` دارد؛ `DJANGO_DEBUG` هم از یونیت
رفت و فقط `.env` می‌ماند). برنامه خودش کلید را در `.django_secret_key` (۶۰۰) ساخت. سنجش: توکنِ جعلی
با کلیدِ داخلِ ریپو برای کاربرهای ۱۰، ۱۱، ۱۲ پیش از تغییر `200` و بعدش `401` گرفت. توکنِ refresh در
دیتابیس است، نه امضاشده، پس نشست‌ها نمی‌میرند و فرانت بی‌صدا توکنِ تازه می‌گیرد؛ این را با ورودِ واقعی
**نسنجیدم**.
- **`CACHE_URL=redis://127.0.0.1:6379/1`** (Redisِ مشترکِ ماشین، بدونِ رمز، `maxmemory 0`؛ دیتابیسِ ۰ مالِ
دیگران است و دست نخورد). سقفِ نرخ حالا مشترک است: رگبارِ ۶۰۰ درخواستی ۱۲۸ تا `200` داد (پیش از آن
۳۶۰ = ۳ worker × ۱۲۰). ⚠️ اگر این Redis بخوابد، هر مسیرِ throttleدار خطا می‌دهد؛ برگشت: خطِ `CACHE_URL` را
از `.env` خالی کنید و ری‌استارت.
- پایانِ درس/جزیره بی‌جواب و روی جزیره‌ی قفل پذیرفته نمی‌شود، ۱۵ مسیر سقفِ نرخ گرفتند، `deploy.sh`
پیش از `migrate` بکاپ می‌گیرد و سلامت را از سوکتِ gunicorn می‌سنجد.
- نسخه‌ی کتابخانه‌های `requirements.txt` قفل شد (نسخه‌های روی سرور).
## وضعیت ۲۰۲۶-۱۰-۰۵ (عصر) — ورودِ مهمان از ریشه حذف شد
**روی سرور از ۲۰۲۶-۱۰-۰۵ ۱۸:۳۹** (بک‌اند `5b7e6fb`، سرویس واقعاً ری‌استارت شد، `accounts.0008` اعمال شد؛ فرانت `0db6526`).

View File

@@ -1,6 +1,6 @@
# پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز مانده‌اند
**آخرین بازبینی با کد: ۲۰۲۶-۱۰-۰۷ (۱۵ مهر ۱۴۰۵)؛ با سرورِ زنده: ۲۰۲۶-۱۰-۰۴؛ موارد دیپلوشده‌ی ۱۰-۰۷ (بک‌اند `7ecb337`، ۱۳:۴۲) فقط در حدِ سقفِ نرخ و سلامت از بیرون سنجیده شد.**
**آخرین بازبینی با کد: ۲۰۲۶-۱۰-۰۷ (۱۵ مهر ۱۴۰۵)؛ با سرورِ زنده: ۲۰۲۶-۱۰-۰۴ برای بقیه‌ی بندها؛ آنچه ۱۰-۰۷ دیپلو شد (بک‌اند `4c60108`، ری‌استارت ۱۳:۵۹) از بیرون سنجیده شد: کلیدِ امضا، Redis، سقفِ نرخ، سلامت.**
این پروژه تا این تاریخ در حالتِ **دمو و ارائه‌ی کارآموزی** است، نه محصولِ نهایی. چند چیز
عمداً باز گذاشته شده تا توسعه‌دهنده‌ها، فرانت‌کار و ارائه کار کنند. هر کدام برای یک سرورِ
@@ -21,20 +21,17 @@
| # | مورد | خطر | کارِ کیست |
|---|---|---|---|
| ۱ | کلیدِ امضای توکن روی سرور همان رشته‌ی داخلِ ریپو است | بحرانی — جعلِ ورود به‌جای هر کاربر | کد آماده؛ اعمال روی سرور |
| ۲ | ۷ حسابِ دمو با کدِ ورودِ ثابت روی پروداکشن | بالا | `.env` سرور |
| ۳ | داده‌ی دمو روی دیتابیسِ پروداکشن | بالا | دیتابیسِ سرور |
| ۴ | `/api/health` جزئیاتِ داخلی را به همه نشان می‌دهد | متوسط | کد |
| ۵ | Swagger، ReDoc و schemaی کاملِ API عمومی‌اند | متوسط | کد |
| ۷ | پنلِ `/admin/` روی دامنه‌ی عمومی | متوسط | nginx + کد |
| ۸ | سرور بدونِ ArvanCloud روی پورتِ ۸۰ هم جواب می‌دهد | متوسط | ادمینِ سرور |
| ۹ | کشِ مشترک (Redis) نیست؛ محدودیت‌ها per-worker‌اند | متوسط | `.env` سرور |
| ۱۱ | کدِ OTP بی‌هش در دیتابیس و در فهرستِ پنل | کم | کد |
| ۱۲ | هزینه‌ی دستیارِ AI با ساختِ شماره‌ی تازه (هر شماره ۳ سوالِ رایگان) قابلِ بالا بردن است | کم (مالی) | کد |
| ۱۴ | SQLite روی دیسکِ مشترکِ پُر؛ بکاپِ پیش از migrate خودکار است ولی بیرون از ماشین نیست | کم (داده) | زیرساخت |
| ۱۵ | دسترسیِ سرور: حسابِ مشترک، رمز، sudoِ کامل | بالا | ادمینِ سرور |
| ۱۶ | فرانت توکن را در `localStorage` نگه می‌دارد | کم | فرانت |
| ۲۰ | یونیتِ systemd `DJANGO_DEBUG=1` و کلیدِ ناامن دارد؛ فقط یک خطِ `.env` جلوی DEBUG را گرفته | بالا (نهفته) | ادمینِ سرور |
| ۲۲ | سایتِ فرانت هیچ هدرِ امنیتی ندارد (CSP، frame-ancestors، HSTS) | کم | nginx/فرانت |
| ۲۴ | سقفِ روزانه‌ی ۳۰۰ پیامکِ کلِ سامانه را یک ناشناس پر می‌کند و ورود با پیامک برای همه بسته می‌شود | بالا (دسترس‌پذیری، هزینه) | تصمیمِ طراحی |
| ۲۵ | با دانستنِ شماره‌ی کسی، ورودِ او تا ۲۴ ساعت بسته می‌شود | متوسط | تصمیمِ طراحی |
@@ -45,24 +42,6 @@
---
## ۱. کلیدِ امضای توکن (`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` سرور. شماره‌ها
@@ -136,15 +115,6 @@
درخواست دور زده می‌شود، و حفاظتِ 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` باشد.
## ۱۱. کدِ OTP بی‌هش در دیتابیس
- **چیست:** `OTPCode.code` متنِ ساده است و در فهرستِ پنلِ ادمین (`OTPCodeAdmin`) دیده می‌شود.
@@ -197,18 +167,6 @@
- **خطر:** هر 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`/
@@ -298,6 +256,8 @@
| پانزده مسیرِ خواندنی و نوشتنیِ کم‌خطر سقفِ نرخ دارند (بانکِ سوال و intro بی‌ورود، `progress/nodes`، `user/hearts`، اعلان، تیکت، پلن‌ها، وضعیتِ اشتراک، سفارش، پیشنهادها، affiliate، تاریخچه و فیدبکِ دستیار، خروج). با نرخِ ۳ در دقیقه درخواستِ چهارم ۴۲۹ می‌گیرد | `content/tests_read_throttles.py` |
| ساختنِ دستیِ `OTPCode` و `Session` در پنل فقط برای ابرکاربر (کارمندِ عادی ۴۰۳) | `accounts/tests_security_review.py` (`StaffCannotMintLoginsTests`) |
| پایانِ درس و پایانِ آزمونِ جزیره جوابِ ثبت‌شده به همه‌ی سوال‌ها می‌خواهند (۴۰۹ `answers_missing`)، روی جزیره‌ی قفل ۴۰۳ `node_locked` می‌دهند (همان قاعده‌ی نقشه)، جزیره یا درسِ بی‌سوال چیزی برای جواب‌دادن ندارد و اگر باز است بی‌امتیاز رد می‌شود (قفل بودنش هنوز چک می‌شود)، و تعدادِ سوالِ آزمون را کلاینت نمی‌تواند کمتر از آزمونِ استانداردِ ۴ تایی بگوید (ستاره‌ی دروغین) | `content/tests_finish_requires_answers.py`، `content/tests_node_completion.py` |
| کلیدِ امضای توکن تصادفی و فقط روی سرور است (`.django_secret_key`، ۶۰۰)؛ نه در `.env`، نه در یونیت؛ توکنِ ساخته‌شده با کلیدِ داخلِ ریپو `401` می‌گیرد؛ `DJANGO_DEBUG` هم در یونیت نیست | دستی (۲۰۲۶-۱۰-۰۷): توکنِ جعلی `200` ← `401`؛ تستِ واحد ندارد |
| کشِ مشترک (Redis، دیتابیسِ ۱) سقفِ نرخ را بینِ workerها مشترک می‌کند | دستی (۲۰۲۶-۱۰-۰۷): رگبارِ ۶۰۰ درخواستی ۱۲۸ تا `200` (پیش‌تر ۳۶۰)؛ تستِ واحد ندارد |
| کتابخانه‌ها بدونِ آسیب‌پذیریِ شناخته‌شده (`urllib3>=2.8.0`، `python-dotenv 1.2.2`) | پایگاهِ OSV روی `pip freeze` |
| actionهای لغو/بازپرداختِ سفارش، علامتِ «بررسی‌شده» و لینکِ بازار اجازه‌ی تغییر می‌خواهند؛ کارمندِ فقط‌خواندنی (Supervisor) اجرایشان نمی‌کند | `payment/tests_security_review.py`، `content/tests_security_review.py` |
| ورودِ فایلِ سوال/برنامه‌ی درسی اجازه‌ی افزودن (و برای جایگزینی، حذف) می‌خواهد؛ مرورگرِ محتوا اجازه‌ی دیدنِ سوال | `content/tests_security_review.py` |

View File

@@ -10,16 +10,17 @@ whitenoise==6.11.0
gunicorn==26.2.0
# وابستگیِ غیرمستقیمِ requests و qdrant-client؛ نسخه‌های پایین‌تر آسیب‌پذیریِ شناخته‌شده دارند
# (GHSA-8988-9cw3-xx77، GHSA-vxq7-64xx-v4gw، GHSA-gh4c-6fx4-qh6g).
urllib3>=2.8.0
urllib3==2.8.0
# نسخه‌ها همان‌هایی‌اند که روی سرور نصب بود (۲۰۲۶-۱۰-۰۷، pip freeze)؛ ارتقا یعنی عوض‌کردنِ عمدیِ یک خط.
# RAG / AI tutor (اپ rag). کلاینت‌ها lazy import می‌شن؛ نبودِ این‌ها فقط
# مسیرِ RAG رو از کار می‌ندازه، نه بقیه‌ی پروژه رو.
openai>=1.40,<3
qdrant-client>=1.12
openai==2.54.0
qdrant-client==1.19.0
# فقط برای ingestِ PDF (اختیاری). pdf2image به poppler سیستمی نیاز داره
# (macOS: brew install poppler). نبودشون فقط endpointِ /api/rag/ingest/pdf
# رو با خطای صریح از کار می‌ندازه.
pymupdf>=1.24
pdf2image>=1.17
pillow>=10
pymupdf==1.28.2
pdf2image==1.17.0
pillow==12.3.0