Server hardening: nginx origin locked to ArvanCloud + CSP, demo logins removed, docs
- ops/nginx/: the two nginx sites as they now run on 249. Only ArvanCloud edges reach the app (direct hits on the IP get 403, so Arvan's DDoS/WAF can't be skipped); the frontend gets CSP (own sources only, the one inline script by hash), X-Frame-Options, nosniff, Referrer-Policy, Permissions-Policy, HSTS; API body limit 100M -> 20M plus HSTS. Checked from outside and in Chrome (all tabs and an island quiz, logged in): no CSP errors. - Demo accounts (users 11-15, 25) and TEST_LOGIN_PHONES removed from the server; the health warning for fixed-code logins now has a test and says how to test instead (an OTPCode made in the admin). - SMS daily cap on the server is 2000 (launch of ~600 users). - CLAUDE.md: never run tests with --parallel on macOS, always --noinput (both hang forever without an error). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# پیش از پروداکشنِ واقعی: چیزهایی که عمداً باز ماندهاند
|
||||
|
||||
**آخرین بازبینی با کد: ۲۰۲۶-۱۰-۰۷ (۱۵ مهر ۱۴۰۵)؛ با سرورِ زنده: ۲۰۲۶-۱۰-۰۴ برای بقیهی بندها؛ آنچه ۱۰-۰۷ دیپلو شد از بیرون و با آزمونِ زنده سنجیده شد: کلیدِ امضا، سقفِ نرخ، پایانِ جزیره/درس، سلامت.**
|
||||
**آخرین بازبینی با کد و سرورِ زنده: ۲۰۲۶-۱۰-۱۳ (۲۱ مهر ۱۴۰۵): حسابهای دمو، nginx (فقط آروان، CSP)، جعلِ IP، سقفِ پیامک.**
|
||||
|
||||
این پروژه تا این تاریخ در حالتِ **دمو و ارائهی کارآموزی** است، نه محصولِ نهایی. چند چیز
|
||||
عمداً باز گذاشته شده تا توسعهدهندهها، فرانتکار و ارائه کار کنند. هر کدام برای یک سرورِ
|
||||
@@ -21,16 +21,12 @@
|
||||
|
||||
| # | مورد | خطر | کارِ کیست |
|
||||
|---|---|---|---|
|
||||
| ۲ | ۷ حسابِ دمو با کدِ ورودِ ثابت روی پروداکشن | بالا | `.env` سرور |
|
||||
| ۳ | دادهی دمو روی دیتابیسِ پروداکشن | بالا | دیتابیسِ سرور |
|
||||
| ۷ | پنلِ `/admin/` روی دامنهی عمومی | متوسط | nginx + کد |
|
||||
| ۸ | سرور بدونِ ArvanCloud روی پورتِ ۸۰ هم جواب میدهد | متوسط | ادمینِ سرور |
|
||||
| ۱۱ | کدِ OTP بیهش در دیتابیس و در فهرستِ پنل | کم | کد |
|
||||
| ۱۲ | هزینهی دستیارِ AI با ساختِ شمارهی تازه (هر شماره ۲ سوالِ رایگان) قابلِ بالا بردن است | کم (مالی) | کد |
|
||||
| ۱۵ | دسترسیِ سرور: حسابِ مشترک، رمز، sudoِ کامل | بالا | ادمینِ سرور |
|
||||
| ۱۶ | فرانت توکن را در `localStorage` نگه میدارد | کم | فرانت |
|
||||
| ۲۲ | سایتِ فرانت هیچ هدرِ امنیتی ندارد (CSP، frame-ancestors، HSTS) | کم | nginx/فرانت |
|
||||
| ۲۴ | سقفِ روزانهی ۳۰۰ پیامکِ کلِ سامانه را یک ناشناس پر میکند و ورود با پیامک برای همه بسته میشود | بالا (دسترسپذیری، هزینه) | تصمیمِ طراحی |
|
||||
| ۲۴ | سقفِ روزانهی پیامکِ کلِ سامانه (۲٬۰۰۰ روی سرور) را یک ناشناس پر میکند و ورود با پیامک برای همه بسته میشود | بالا (دسترسپذیری، هزینه) | تصمیمِ طراحی |
|
||||
| ۲۵ | با دانستنِ شمارهی کسی، ورودِ او تا ۲۴ ساعت بسته میشود | متوسط | تصمیمِ طراحی |
|
||||
| ۲۷ | کلیدِ جوابِ هر سوال با یک حسابِ دوم درمیآید؛ امتیازِ هر جوابِ درست دو بار داده میشود | کم | تصمیمِ محصولی |
|
||||
| ۲۸ | استفادهی دوباره از توکنِ تمدیدِ دزدیدهشده تشخیص داده نمیشود؛ «خروج از همهی دستگاهها» نیست | کم | کد، بعد از ارائه |
|
||||
@@ -39,51 +35,14 @@
|
||||
|
||||
---
|
||||
|
||||
## ۲. حسابهای دمو با کدِ ورودِ ثابت (`TEST_LOGIN_PHONES`)
|
||||
|
||||
- **چیست:** ۷ شمارهی `09120000101` تا `09120000107` با کدِ ثابت در `.env` سرور. شمارهها
|
||||
در ریپو (`docs/operations.md`، `CLAUDE.md`) آمدهاند؛ کدها فقط در راهنمای ارائهی صاحبِ
|
||||
پروژهاند.
|
||||
- **خطر:** کسی که کد را از راهنما، اسکرینشات یا ارائه ببیند وارد آن حسابها میشود. این
|
||||
کدها با `OTP_MAX_FAILED_ATTEMPTS` باطل نمیشوند (ردیفِ `OTPCode` ندارند)؛ فقط سقفِ
|
||||
per-workerِ تلاشِ ورود جلویشان است.
|
||||
- **رفع:** `TEST_LOGIN_PHONES` را از `.env` سرور پاک کنید و ریاستارت کنید.
|
||||
- **چطور بسنجید:** `/api/health` دیگر هشدارِ «شمارههای تستِ ورود با کدِ ثابت فعالاند» ندارد.
|
||||
|
||||
## ۳. دادهی دمو روی دیتابیسِ پروداکشن
|
||||
|
||||
- **چیست:** کاربرهای ۱۰ تا ۱۵ و ۲۵ (حسابهای دمو با کدِ ثابت، بند ۲) با درس، گفتگو، تیکت و
|
||||
سفارش روی دیتابیسِ پروداکشن ساخته شدهاند. حسابِ ۱۰ («علی رضایی»، با ۷ سفارش) حسابِ نمایشیِ اصلی است.
|
||||
- **خطر:** دادهی ساختگی در آمار و جدولِ رتبهبندیِ واقعی.
|
||||
- **رفع:** پیش از کاربرِ واقعی با `delete_users --ids … --yes` (پیش از آن بکاپ) پاک شوند.
|
||||
`purge_demo_data` اینها را **نمیگیرد** (از مسیرِ واقعیِ viewها ساخته شدهاند).
|
||||
- **چطور بسنجید:** `/api/health` → `traffic.users` فقط کاربرانِ واقعی را بشمارد.
|
||||
|
||||
## ۷. پنلِ ادمین روی دامنهی عمومی
|
||||
|
||||
- **چیست:** `/admin/` روی `mathapi.tadrisino2.ir`، با ورودِ نامکاربری/رمزِ جنگو. سقفِ ۱۰ تلاش در ۱۵ دقیقه برای هر IP دارد (`AdminLoginThrottleMiddleware`)،
|
||||
ولی: IP با XFF جعلپذیر است (مورد ۸)، و ۲FA ندارد.
|
||||
IP جعلپذیر نیست (فقط از آروان)، ولی ۲FA و محدودیتِ IP ندارد.
|
||||
- **خطر:** ادمین همهی محتوا، پلنها، کدهای تخفیف، سفارشها و کاربرها را کنترل میکند.
|
||||
- **رفع:** محدود کردنِ `/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.
|
||||
|
||||
- **دور زدنِ سقفها بسته شد:** `TrustedProxyMiddleware` فقط XFFِ رسیده از رنجهای آروان را میپذیرد (جدولِ حفاظها).
|
||||
آنچه مانده: سپرِ DDoS/WAFِ آروان برای درخواستِ مستقیم به IP وجود ندارد.
|
||||
- **بازتولید شد ۲۰۲۶-۱۰-۰۴:** `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 که در دیتابیس شمرده میشوند متأثر نیستند. پورتهای ۸۰ و ۴۴۳ هر دو باز بودند.
|
||||
|
||||
## ۱۱. کدِ OTP بیهش در دیتابیس
|
||||
|
||||
- **چیست:** `OTPCode.code` متنِ ساده است و در فهرستِ پنلِ ادمین (`OTPCodeAdmin`) دیده میشود.
|
||||
@@ -121,23 +80,13 @@
|
||||
- **چیست:** کلاینتِ فرانت (`src/api/index.ts` در مخزنِ فرانت) توکن و refresh را در
|
||||
`localStorage` نگه میدارد.
|
||||
- **خطر:** هر XSS در سایت هر دو را میخواند.
|
||||
- **رفع:** CSPِ سختگیرانه روی `math.tadrisino2.ir`؛ در بلندمدت refresh در کوکیِ `HttpOnly`.
|
||||
|
||||
## ۲۲. هدرهای امنیتیِ فرانت
|
||||
|
||||
- **چیست:** `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`.
|
||||
- **رفع:** CSPِ سختگیرانه روی `math.tadrisino2.ir` حالا هست (جدولِ حفاظها) و اثرِ XSS را کم میکند؛ مانده: refresh در کوکیِ `HttpOnly`.
|
||||
|
||||
## ۲۴. پر کردنِ سقفِ سراسریِ پیامک
|
||||
|
||||
- **چیست:** `OTP_SMS_GLOBAL_DAILY_MAX` (۳۰۰ در ۲۴ ساعت) جلوی خرجِ بیسقفِ پیامک را میگیرد. هر
|
||||
ناشناسی با درخواستِ کد برای شمارههای تصادفی آن را پر میکند؛ از یک IP بیجعل ساعتی ۲۰ درخواست،
|
||||
یعنی روزی ۴۸۰، پس یک نفر سقف را همیشه پر نگه میدارد.
|
||||
- **چیست:** `OTP_SMS_GLOBAL_DAILY_MAX` (روی سرور ۲٬۰۰۰ در ۲۴ ساعت؛ پیشفرضِ کد ۳۰۰) جلوی خرجِ بیسقفِ پیامک را میگیرد. هر
|
||||
ناشناسی با درخواستِ کد برای شمارههای تصادفی آن را پر میکند؛ از یک IP بیجعل ساعتی ۱۰۰ درخواست؛
|
||||
با چند IP سقف پر میشود.
|
||||
- **بازتولید شد:** ۳۰۰ درخواست پذیرفته و ۳۰۰ پیامک فرستاده شد؛ بعد درخواستِ کاربرِ واقعی ۵۰۳.
|
||||
- **خطر:** هیچ ثبتنامِ تازه، ورودِ دوباره یا حذفِ حسابی ممکن نیست؛ هزینهی ۳۰۰ پیامک در روز.
|
||||
- **رفع (تصمیمِ طراحی):** بعد از گذشتن از یک حد، CAPTCHA یا مانعِ مشابه پیش از ارسال؛ شمارشِ سقف
|
||||
@@ -238,6 +187,9 @@
|
||||
| شمارهی کاربر را فقط ابرکاربر عوض میکند؛ حسابهای کارمند و گروهها فقط با ابرکاربر ساخته/تغییر میکنند | `accounts/tests_security_review.py` (`StaffCannotSwapLoginPhoneTests`، `StaffAccountsAdminSuperuserOnlyTests`) |
|
||||
| لاگِ تحویلِ کمپین اجازهی view میخواهد (هر کارمندی نمیبیند) | `campaigns/tests_security_review.py` |
|
||||
| هر رقمِ یونیکد (تمامعرض، ریاضی…) در شماره به لاتین برمیگردد؛ کدِ غیرِ ASCII، پایهی ناموجود یا عظیم و تاریخِ تولدِ با «²» ۴۰۰ است نه ۵۰۰؛ صافیِ نامِ نمایشی ارقامِ یونیکد و جداکنندهی دلخواه را میگیرد | `accounts/tests_security_review.py` (`UnicodeInputsAreNormalisedOrRejectedTests`) |
|
||||
| روی سرور هیچ شمارهای با کدِ ورودِ ثابت نیست (`TEST_LOGIN_PHONES` خالی)؛ حسابهای دمو و دادهشان پاک شدند؛ اگر کسی آن را دوباره پر کند `/api/health` هشدار میدهد | `config/tests.py` (هشدارِ health)؛ روی سرور: `/api/health` بیهشدار |
|
||||
| فقط ArvanCloud به nginxِ دو سایتِ ما میرسد؛ درخواستِ مستقیم به IPِ سرور ۴۰۳ (`ops/nginx/zareian-arvan-only.conf`) | از بیرون: پورتِ ۸۰ و ۴۴۳ مستقیم ۴۰۳، از راهِ آروان ۲۰۰ |
|
||||
| فرانت CSP (فقط منابعِ خودمان، اسکریپتِ درونخطی با هش)، `frame-ancestors 'none'`، `X-Frame-Options`، `nosniff`، HSTS و `Permissions-Policy` دارد؛ سقفِ بدنهی nginxِ API ۲۰ مگابایت | `curl -sI`؛ اپ در Chrome با و بی ورود: صفر خطای CSP |
|
||||
| X-Forwarded-For فقط وقتی پذیرفته میشود که peerِ TCP (هدرِ `X-Real-IP`ِ nginx) در `TRUSTED_PROXY_NETWORKS` (رنجهای ArvanCloud) باشد؛ درخواستِ مستقیم به IP با XFFِ جعلی دیگر سقفهای IP-محور را دور نمیزند | `config/tests_trusted_proxy.py`؛ روی سرور: ۶۵ درخواستِ health با XFFِ چرخان، بعد از ۶۰ تا ۴۲۹ |
|
||||
| `/api/home?gradeId=` ناموجود کش نمیشود (کشِ پُر شمارندهی سقفِ ورودِ ادمین را پاک میکرد)؛ `/api/progress/nodes` یک کوئری است و `unit` عظیم ۴۰۰ | `content/tests_security_review.py` (`JunkGradeIdsDoNotFlushCacheTests`، `ProgressNodesInputAndQueriesTests`) |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user