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:
45
CLAUDE.md
45
CLAUDE.md
@@ -70,8 +70,8 @@ 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: seven
|
||||
fixed-code demo logins exist, demo users sit in the production DB, and the leaderboard and `/admin/` are public (`/api/health` shows only ok/error without `X-Internal-Key`; Swagger/ReDoc/schema need an admin session since 2026-10-08). (The token signing key is no longer the
|
||||
Since 2026-10-13 there are no fixed-code logins and no demo users on the server (`TEST_LOGIN_PHONES`
|
||||
empty; users 11–15 and 25 deleted; test logins are an `OTPCode` made in the admin). Still open: the leaderboard and `/admin/` are public (`/api/health` shows only ok/error without `X-Internal-Key`; Swagger/ReDoc/schema need an admin session since 2026-10-08). (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).
|
||||
@@ -82,11 +82,20 @@ and when one is closed, move it to that file's «حفاظهایی که هست
|
||||
source venv/bin/activate
|
||||
python manage.py runserver
|
||||
python manage.py migrate
|
||||
python manage.py test # all tests
|
||||
python manage.py test accounts # single app
|
||||
python manage.py test accounts.tests.SomeTestCase.test_something # single test
|
||||
python manage.py test --noinput # all tests (~60 s, serial)
|
||||
python manage.py test --noinput accounts # single app
|
||||
python manage.py test --noinput accounts.tests.SomeTestCase.test_something # single test
|
||||
```
|
||||
|
||||
**Never run the suite with `--parallel` on this Mac, and always pass `--noinput`.** Both hang forever
|
||||
without an error (2026-10-13, cost 30+ minutes): the test DB is a SQLite *file* (`.test-db.sqlite3`,
|
||||
needed by the real-concurrency tests), and on macOS Django's spawned parallel workers fail with
|
||||
`unable to open database file` while the parent waits for them indefinitely. Separately, a test DB left
|
||||
behind by a killed run makes Django ask «Type 'yes' to delete the old test database» on stdin; in a
|
||||
background shell nobody answers and it waits forever. `--noinput` deletes it. Serial runs take ~60 s
|
||||
for the whole suite, so there is nothing to gain. If a run prints nothing for more than ~3 minutes it
|
||||
is stuck, not slow: kill it (`pkill -f "manage.py test"`) and check the log for the two causes above.
|
||||
|
||||
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 since 2026-10-08 it points at our own Redis (`zareian-redis.service`, port 6390, localhost, password-protected, 256 MB `allkeys-lru`, no persistence), never the company's Redis on 6379. Its outage is logged and rate limits stop applying (fail-open); not tested under real Redis failure on the server.
|
||||
|
||||
No pytest; uses Django's built-in test runner. No linter/formatter configured.
|
||||
@@ -451,6 +460,22 @@ 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.**
|
||||
|
||||
## بازبینیِ پروداکشن و سختکردنِ سرور — ۲۰۲۶-۱۰-۱۳ (بکاند `e3bddb3` و بعد)
|
||||
|
||||
چکلیستِ سینیور: `~/Desktop/گزارشات Tadrisino/گزارش-۳۴/` (HTML و PDF؛ ساز در scratchpad، نه ریپو).
|
||||
|
||||
- **جعلِ IP بسته شد:** `config.middleware.TrustedProxyMiddleware` فقط XFFِ رسیده از رنجهای آروان را میپذیرد
|
||||
(`TRUSTED_PROXY_NETWORKS`؛ X-Real-IP را nginx میگذارد). پیش از آن درخواستِ مستقیم به IP با XFFِ چرخان همهی سقفهای
|
||||
IP-محور را دور میزد (روی سرور بازتولید شد).
|
||||
- **nginx (دو سایتِ ما، نسخه در `ops/nginx/`)**: فقط IPهای آروان (مستقیم ۴۰۳)، CSP و هدرهای امنیتی روی فرانت، سقفِ بدنهی
|
||||
API ۲۰ مگابایت، HSTS. بکاپِ قبلی: `.deploy-backups/nginx-*-before-20261013-nginx-hardening`. سنجیده: از بیرون، و اپ در Chrome
|
||||
(همهی تبها و آزمونِ جزیره، با ورود) بی هیچ خطای CSP. نسنجیده: WebViewِ اندروید و `pay.html` تا درگاه.
|
||||
- **حسابهای دمو پاک شدند** (۱۱–۱۵، ۲۵) و `TEST_LOGIN_PHONES` خالی؛ بکاپ `.deploy-backups/db-before-demo-removal-20261013-190632.pgdump`
|
||||
و `env-before-demo-removal-20261013-190632`. سقفِ پیامکِ روزانه ۲٬۰۰۰. اعتبارِ کاوهنگار آن روز ۲۵۳٬۸۴۱ ریال (شارژ: سینیور).
|
||||
- **CPUِ ماشین:** `metabase` و `celerybeat`ِ Artina (مالِ دیگران) بیوقفه ریاستارت میشوند (۱۷۶ و ~۱۲۰۰ بار در ساعت) و
|
||||
حدودِ ۳ از ۸ هسته را میگیرند. به آنها دست نزن؛ درخواست از ادمینِ سرور.
|
||||
- `/etc/nginx` را classifierِ حالتِ خودکار فقط با تأییدِ صریحِ مالک و گامبهگام (بکاپ → نصب و `nginx -t` → reload) اجازه داد.
|
||||
|
||||
## کافهبازار: اشتراک، تخفیفِ پویا، تمدیدِ خودکار — روی سرور از ۲۰۲۶-۱۰-۱۲ ۱۷:۵۴ (بکاند `8077ab4`، فرانت `fffdeac`)
|
||||
|
||||
- **محصولهای پیشخان** (پرسشِ API با توکنِ ساختگی، ۱۰۰۰۰–۱۰۶۹۹): فقط `10342` (یکماهه، تأییدِ سینیور)، `10343` (۹۰ روزه)،
|
||||
@@ -535,8 +560,8 @@ anyone who can read this repo can forge a token for any user.**
|
||||
۱۱۲ درخواست در ثانیه، p99 ۰٫۶۷ ثانیه، وقتی CPUِ ماشین ~۸۰٪ مشغول بود. ثبتِ جواب ۱۵٫۴→۱۱ ms، `/api/home` ۱۲٫۴→۸٫۶ ms.
|
||||
- **برآورد (نه سنجیده):** ۱۰۰۰ تا ۱۵۰۰ بازیکنِ همزمان (فرض: یک درخواست در ۸ ثانیه)، ۴ تا ۶ هزار آنلاین. سقفِ واقعی CPUِ
|
||||
مشترک است، نه حافظه یا دیتابیس؛ worker بیشتر روی همین ماشین ظرفیت نمیسازد، CPUِ دیگران را میگیرد.
|
||||
- **سقفهای تنظیمی:** پیامک ۳۰۰ در روز برای کلِ سامانه و دستیار ۳۰۰۰ سوال در روز — هر دو برای روزِ لانچ کماند و
|
||||
**تصمیمِ مالک/سینیور** است (`OTP_SMS_GLOBAL_DAILY_MAX`، `AI_GLOBAL_DAILY_MAX` در `.env`؛ هیچکدام الان در `.env` نیست).
|
||||
- **سقفهای تنظیمی:** پیامکِ کلِ سامانه روی سرور ۲٬۰۰۰ در روز (`OTP_SMS_GLOBAL_DAILY_MAX` در `.env`، از ۲۰۲۶-۱۰-۱۳ به
|
||||
تصمیمِ مالک برای لانچِ ۶۰۰ نفره؛ پیشفرضِ کد ۳۰۰) و دستیار ۳۰۰۰ سوال در روز (`AI_GLOBAL_DAILY_MAX`، در `.env` نیست).
|
||||
هر IP: ۱۰۰ درخواستِ کد و ۳۰۰ ثبتِ کد در ساعت (تصمیمِ مالک، برای کلاس روی یک وایفای).
|
||||
- **پیشنهادشده، ساخته نشده:** جلسهی آزمون به سبکِ دولینگو (سوال و کلید با هم دانلود، سنجش روی گوشی، ارسالِ یکجا
|
||||
و سنجشِ دوباره در سرور) بارِ بازی را حدوداً نصف میکند؛ کلید روی گوشی میرود، پس تصمیمِ محصولی است. کلِ محتوا
|
||||
@@ -1013,10 +1038,8 @@ migrationها اعمال شدند، سهیار و `nextLessonId` از بیر
|
||||
|
||||
سه چیزِ کوچک که در همان روز اضافه شد و دانستنشان لازم است:
|
||||
|
||||
- **`TEST_LOGIN_PHONES`** — فهرستِ «شماره:کد» در `.env` که ورود بدونِ کاوهنگار را
|
||||
ممکن میکند. از ۲۰۲۶-۰۹-۲۳ شب، به تصمیمِ صاحبِ پروژه، روی سرور **۷ شمارهی دمو**
|
||||
دارد (`09120000101` تا `09120000107`). کدها فقط در راهنمای ارائهی خودِ صاحبِ پروژهاند،
|
||||
نه در ریپو. جزئیات در `docs/operations.md`.
|
||||
- **`TEST_LOGIN_PHONES`** — فهرستِ «شماره:کد» برای ورود بیپیامک. **از ۲۰۲۶-۱۰-۱۳ روی سرور خالی است**
|
||||
(تصمیمِ مالک: کدِ ثابت نکتهی امنیتی است) و حسابهای دمو پاک شدند؛ `docs/operations.md` «ورود بیپیامک».
|
||||
- **RAG روی سرور روشن است** (همان شب): `OPENAI_API_KEY`/`QDRANT_API_KEY` در `.env`
|
||||
سرور، `/api/health` میدهد `qdrant: true, openai: true`. هر سؤالِ کاربرِ واقعی از
|
||||
اعتبارِ GapGPT خرج میکند؛ سقفِ ۱۵ در روز برای هر کاربر آن را محدود نگه میدارد.
|
||||
|
||||
@@ -316,8 +316,8 @@ def _warnings() -> list[str]:
|
||||
# تعداد عمداً گفته نمیشود: این صفحه عمومی است و «۷ حسابِ کدِ ثابت» برای
|
||||
# مهاجم یعنی هدفِ مشخص برای حدس زدن.
|
||||
out.append(
|
||||
'شمارههای تستِ ورود با کدِ ثابت فعالاند (TEST_LOGIN_PHONES). '
|
||||
'برای توسعه لازم است؛ پیش از تحویلِ نهایی به کاربرانِ واقعی حذفشان کنید.'
|
||||
'شمارههای تستِ ورود با کدِ ثابت فعالاند (TEST_LOGIN_PHONES). روی سرور باید خالی باشد: '
|
||||
'هر کس کد را دیده باشد وارد میشود. برای آزمون یک OTPCode در پنلِ ادمین بسازید.'
|
||||
)
|
||||
if not settings.DEBUG and settings.SECRET_KEY.startswith('django-insecure'):
|
||||
# بزرگترین ریسکِ باز (docs/security-before-production.md): این کلید در گیت است و هر
|
||||
|
||||
20
config/tests_no_fixed_logins.py
Normal file
20
config/tests_no_fixed_logins.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
روی سرور هیچ شمارهای با کدِ ورودِ ثابت نباید باشد (حسابهای دمو ۲۰۲۶-۱۰-۱۳ پاک شدند). اگر کسی
|
||||
TEST_LOGIN_PHONES را دوباره پر کند، /api/health (با کلید) باید بگوید، نه بیصدا بپذیرد.
|
||||
"""
|
||||
|
||||
from django.test import TestCase, override_settings
|
||||
|
||||
|
||||
@override_settings(INTERNAL_SERVICE_KEY='k')
|
||||
class FixedCodeLoginsAreFlaggedTests(TestCase):
|
||||
def _warnings(self):
|
||||
return self.client.get('/api/health', HTTP_X_INTERNAL_KEY='k').json()['warnings']
|
||||
|
||||
@override_settings(TEST_LOGIN_PHONES={'09120000001': '834512'})
|
||||
def test_a_fixed_code_phone_raises_a_warning(self):
|
||||
self.assertTrue(any('TEST_LOGIN_PHONES' in w for w in self._warnings()))
|
||||
|
||||
@override_settings(TEST_LOGIN_PHONES={})
|
||||
def test_empty_list_is_quiet(self):
|
||||
self.assertFalse(any('TEST_LOGIN_PHONES' in w for w in self._warnings()))
|
||||
@@ -220,7 +220,7 @@ curl https://mathapi.tadrisino2.ir/api/health
|
||||
| `ZARINPAL_REDIRECT_URL` | بعد از پرداخت کاربر به `FRONTEND_URL/?orderId=…&status=paid\|failed` برمیگردد. `ZARINPAL_REDIRECT_URL=-` یعنی همان پاسخِ JSONِ قدیمی |
|
||||
| `BAZAAR_PISHKHAN_API_SECRET` (روشِ جدید) یا `BAZAAR_CLIENT_ID` · `BAZAAR_CLIENT_SECRET` (روشِ قدیمی)، بهعلاوهی `BAZAAR_PACKAGE_NAME` | `POST /api/payment/bazaar/verify` هر خرید را نامعتبر میداند (fail-closed)؛ `/api/health` → `bazaar: false` |
|
||||
| `OPENAI_API_KEY` · `QDRANT_API_KEY` | `POST /api/ai/ask` همان پاسخِ stub را میدهد |
|
||||
| `TEST_LOGIN_PHONES` | هیچ حسابِ تستی باز نیست (بخشِ بعد) |
|
||||
| `TEST_LOGIN_PHONES` | خالی، و روی سرور باید خالی بماند (بخشِ «ورود بیپیامک») |
|
||||
| `HEARTS_ENFORCED` | روشن (از ۲۰۲۶-۱۰-۰۲): با صفر قلب نه جواب پذیرفته میشود نه پایانِ درس/جزیرهی بیجواب. `0` خاموشش میکند |
|
||||
| `INTERNAL_SERVICE_KEY` | endpointهای داخلیِ ingestِ RAG و ساختِ سفارشِ سرور-به-سرور بستهاند (در DEBUG هم؛ پیشفرض ندارد) |
|
||||
|
||||
@@ -281,7 +281,9 @@ callbackِ زرینپال، برگشت از پرداخت، `BAZAAR_REDIRECT_U
|
||||
4. **فرانت:** روی سرورِ فرانت در `dist/config.js` بنویسید
|
||||
`apiBaseUrl: "https://<دامنهی تازهی API>/api"` (یا `.env.production` را عوض و
|
||||
build کنید)، و کشِ آروان را برای `config.js` و `index.html` پاک کنید.
|
||||
5. **وارسی:** `curl https://<دامنهی تازهی API>/api/health` (در `clientIp` باید IPِ
|
||||
5. **nginx و CSP:** در `ops/nginx/zareian-front-headers.conf` دامنهی تازهی API را در `connect-src` بگذارید،
|
||||
روی سرور در `/etc/nginx/snippets/` نصب کنید (بخشِ «nginx»)؛ وگرنه مرورگر هر درخواستِ API را میبندد.
|
||||
6. **وارسی:** `curl https://<دامنهی تازهی API>/api/health` (در `clientIp` باید IPِ
|
||||
خودتان باشد) و یک بار ورود و باز کردنِ یک درس در اپ.
|
||||
|
||||
مرحلهی ۳ را فقط یک پرداختِ واقعی ثابت میکند؛ تا آن انجام نشده، پرداخت را
|
||||
@@ -344,53 +346,19 @@ callbackِ زرینپال، برگشت از پرداخت، `BAZAAR_REDIRECT_U
|
||||
تستها: `payment/tests_bazaar_subscription.py`، `payment/tests_bazaar_oauth.py`.
|
||||
**هیچ خریدِ واقعیای از بازار انجام نشده**؛ نه پذیرفتنِ JWT از سوی بازار و نه تمدیدِ واقعی.
|
||||
|
||||
## ورودِ توسعهدهندهها (بدونِ سرویسِ پیامک)
|
||||
## ورود بیپیامک (آزمون)
|
||||
|
||||
تا وقتی `KAVENEGAR_API_KEY` ست نشده، با `DEBUG=0` هیچکس نمیتواند با شماره
|
||||
وارد شود. برای اینکه ورودِ تیم به یک نفرِ خاص وابسته نباشد، چند شمارهی تست با
|
||||
کدِ ثابت تعریف کنید:
|
||||
**روی سرور هیچ شمارهای با کدِ ثابت نیست** و نباید باشد: کدِ ثابت هر کس آن را در ارائه یا اسکرینشات
|
||||
دیده باشد را وارد میکند. `TEST_LOGIN_PHONES` در `.env` سرور خالی است و `/api/health` اگر پر شود هشدار
|
||||
میدهد. حسابهای دمو (کاربرهای ۱۱ تا ۱۵ و ۲۵) و همهی دادهشان با `delete_users` پاک شدند (بکاپِ پیش از آن:
|
||||
`.deploy-backups/db-before-demo-removal-20261013-190632.pgdump`). حسابِ ۱۰ به شمارهی مالکِ پروژه وصل است و ماند.
|
||||
|
||||
```bash
|
||||
TEST_LOGIN_PHONES=09120000001:834512,09120000002:110947
|
||||
```
|
||||
راهِ آزمون بیپیامک: یک ابرکاربر در `/admin/accounts/otpcode/add/` برای شمارهی خودش یک کدِ تصادفی با انقضای
|
||||
چند دقیقه میسازد (یکبارمصرف، با همان سقفِ ۵ حدس). کارمندِ عادی این صفحه را ندارد
|
||||
(`StaffCannotMintLoginsTests`). بعد از آزمون `POST /api/auth/logout`.
|
||||
|
||||
برای این شمارهها پیامکی ارسال نمیشود و همان کد همیشه معتبر است. هر شمارهی
|
||||
دیگری مسیرِ عادیِ OTP را میرود، بیهیچ تغییری.
|
||||
|
||||
```bash
|
||||
curl -X POST https://mathapi.tadrisino2.ir/api/auth/otp/request \
|
||||
-H 'Content-Type: application/json' -d '{"phone":"09120000001"}'
|
||||
curl -X POST https://mathapi.tadrisino2.ir/api/auth/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"phone":"09120000001","otp":"834512"}'
|
||||
```
|
||||
|
||||
این همان الگوییست که Firebase Auth (با نامِ fictional phone numbers) و Apple
|
||||
(حسابِ دمو برای بررسیِ App Store) برای همین مسئله دارند.
|
||||
|
||||
- **با `DEV_MASTER_OTP` یکی نیست.** آن کدِ `0000` **هر** شمارهای را میپذیرد و
|
||||
فقط با `DEBUG=1` روشن میشود. این فقط همین چند شماره را، هرکدام با کدِ خودش.
|
||||
- **کد در پاسخِ API برنمیگردد**، پس دانستنِ شماره برای ورود کافی نیست. سقفِ
|
||||
تلاشِ ورودِ هر شماره روی اینها هم اعمال میشود، پس حدسِ کد بسته است.
|
||||
- **مقدارِ نامعتبر سرویس را بالا نمیآورد** و پیامِ صریح میدهد — بیصدا رد
|
||||
نمیشود. کد باید ۴ تا ۶ رقم باشد (`LoginRequestSerializer` سقفِ ۶ کاراکتر
|
||||
دارد).
|
||||
- **`/api/health` تا وقتی پر باشد هشدار میدهد** — فقط تعداد، نه شماره و نه کد.
|
||||
- شماره را **دقیقاً** همانطور بنویسید که کلاینت میفرستد؛ این کدبیس شماره را
|
||||
normalize نمیکند، پس `09121111111` و `+989121111111` دو کاربرِ جدا هستند.
|
||||
- کدِ تصادفی بگذارید، نه `1234`، و پیش از باز شدنِ سرویس روی کاربرانِ واقعی
|
||||
خالیاش کنید.
|
||||
|
||||
### حسابِ نمایشی
|
||||
|
||||
حسابِ «علی رضایی» (کاربرِ ۱۰) پُر از داده است و با شمارهی مالکِ پروژه وصل میشود؛ بقیهی دموها با `TEST_LOGIN_PHONES` وارد میشوند.
|
||||
|
||||
⚠️ **این داده دستی روی دیتابیسِ پروداکشن نوشته شده و در گیت نیست.** پیش از باز
|
||||
شدنِ سرویس روی کاربرانِ واقعی باید برود، وگرنه اعلانها و لیدربردِ ساختگی را
|
||||
کاربرِ واقعی میبیند. `purge_demo_data` اینها را **نمیگیرد** — آن فرمان فقط
|
||||
دنبالِ پیشوندِ `[SEED]`/`[DEMO]` روی Grade/Plan/Character/User میگردد. پاک
|
||||
کردن یعنی حذفِ دستیِ ردیفهای `Notification` و `LeaderboardEntry` از پنلِ
|
||||
ادمین (اول بکاپ، بخشِ بالا).
|
||||
روی لپتاپ (`DEBUG=1`) کدِ `devOtp` در پاسخ میآید و `TEST_LOGIN_PHONES=09120000001:834512` هم کار میکند؛
|
||||
هیچکدام برای سرور نیست.
|
||||
|
||||
## سرور
|
||||
|
||||
@@ -422,6 +390,29 @@ sudo systemctl restart gunicorn # مالِ ما نیست — اپِ
|
||||
5. **`df -h /` را قبل از هر کارِ پرنوشت چک کنید.** دیسک مدتیست حدودِ ۹۴٪ پر
|
||||
است؛ پر شدنش سرویسِ ایمیل و Gitea را هم با ما پایین میکشد.
|
||||
|
||||
## nginx (دو سایتِ ما)
|
||||
|
||||
نسخهی اصلی در ریپو است: `ops/nginx/`. روی ۲۴۹: `sites-enabled/mathapp` (API، فایلِ معمولی)،
|
||||
`sites-available/mathapp-front` (فرانت)، و دو snippet در `/etc/nginx/snippets/`:
|
||||
|
||||
| فایل | چه میکند |
|
||||
|---|---|
|
||||
| `zareian-arvan-only.conf` | فقط IPهای ArvanCloud و خودِ ماشین؛ درخواستِ مستقیم به IPِ سرور ۴۰۳. همان فهرستِ `TRUSTED_PROXY_NETWORKS` |
|
||||
| `zareian-front-headers.conf` | CSP، `X-Frame-Options`، `nosniff`، `Referrer-Policy`، `Permissions-Policy`، HSTS برای فرانت |
|
||||
| `mathapp` | سقفِ بدنه ۲۰ مگابایت، HSTS |
|
||||
|
||||
تغییر: فایلِ ریپو را ویرایش و کامیت کنید؛ روی سرور فایلِ فعلی را در `.deploy-backups/` کپی کنید، `sudo install -m 644`،
|
||||
`sudo nginx -t`، و فقط اگر موفق بود `sudo systemctl reload nginx` (نه restart: سایتهای دیگران روی همین nginxاند).
|
||||
بعد از بیرون: هدرها با `curl -sI`، و درخواستِ مستقیم به IP باید ۴۰۳ بدهد.
|
||||
|
||||
**وقتی دیگران به اینها بخورند:**
|
||||
- آروان رنجِ IPِ تازه گرفته: کاربرانِ پشتِ آن edge ۴۰۳ میگیرند و peerشان در `/var/log/nginx/access.log` است. فهرست را
|
||||
از <https://www.arvancloud.ir/fa/ips.txt> در snippet و در `config/settings.py` با هم تازه کنید.
|
||||
- اسکریپتِ درونخطیِ `index.html`ِ فرانت عوض شده: CSP آن را میبندد (کنسول: «Refused to execute inline script»). هشِ تازه را در
|
||||
snippet بگذارید (روش در خودِ فایل).
|
||||
- سنجیده (۲۰۲۶-۱۰-۱۳): اپ در Chrome، بیورود و با ورود، همهی تبها و آزمونِ یک جزیره، بی هیچ خطای CSP. **نسنجیده:** اپِ اندروید
|
||||
(WebView همان CSP را میگیرد) و صفحهی `pay.html` تا درگاه.
|
||||
|
||||
## لاگها
|
||||
|
||||
هر درخواستِ `/api/` بهصورتِ یک شیءِ JSON در هر خط لاگ میشود
|
||||
|
||||
@@ -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`) |
|
||||
|
||||
|
||||
23
ops/nginx/mathapp
Normal file
23
ops/nginx/mathapp
Normal file
@@ -0,0 +1,23 @@
|
||||
# روی ۲۴۹: /etc/nginx/sites-enabled/mathapp (فایلِ معمولی، نه symlink) — API، mathapi.tadrisino2.ir.
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
server_name mathapi.tadrisino2.ir;
|
||||
|
||||
ssl_certificate /etc/ssl/certs/mathapp.crt;
|
||||
ssl_certificate_key /etc/ssl/private/mathapp.key;
|
||||
|
||||
include /etc/nginx/snippets/zareian-arvan-only.conf;
|
||||
|
||||
# بزرگترین آپلودِ واقعی فایلِ سوال/برنامهی درسی در پنلِ ادمین است (~۹ مگابایت). nginx بدنه را پیش از
|
||||
# رد شدن روی دیسکِ مشترک مینویسد، پس سقفِ ۱۰۰ مگابایتیِ قبلی جای پر کردنِ دیسک میداد.
|
||||
client_max_body_size 20M;
|
||||
|
||||
# بقیهی هدرهای امنیتی (nosniff، X-Frame-Options، Referrer-Policy) را خودِ جنگو میگذارد.
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
|
||||
location / {
|
||||
include proxy_params;
|
||||
proxy_pass http://unix:/run/gunicorn/mathapp.sock;
|
||||
}
|
||||
}
|
||||
31
ops/nginx/mathapp-front
Normal file
31
ops/nginx/mathapp-front
Normal file
@@ -0,0 +1,31 @@
|
||||
# روی ۲۴۹: /etc/nginx/sites-available/mathapp-front (sites-enabled یک symlink به آن است) — math.tadrisino2.ir.
|
||||
server {
|
||||
listen 80;
|
||||
listen 443 ssl;
|
||||
server_name math.tadrisino2.ir;
|
||||
ssl_certificate /etc/ssl/certs/mathapp.crt;
|
||||
ssl_certificate_key /etc/ssl/private/mathapp.key;
|
||||
root /home/administrator/mathapp_frontend_new/dist;
|
||||
|
||||
include /etc/nginx/snippets/zareian-arvan-only.conf;
|
||||
include /etc/nginx/snippets/zareian-front-headers.conf;
|
||||
|
||||
# index.html و config.js اسم ثابت دارند: مرورگر باید هر بار از سرور بپرسد (etag → ۳۰۴)،
|
||||
# وگرنه بعد از دیپلوی نسخهی قدیمی را نشان میدهد.
|
||||
add_header Cache-Control "no-cache" always;
|
||||
|
||||
location / {
|
||||
index index.html;
|
||||
try_files $uri $uri/ @fallback;
|
||||
}
|
||||
|
||||
# فایلهای assets/ اسمشان هش دارد و هیچوقت با همان اسم عوض نمیشوند.
|
||||
location /assets/ {
|
||||
include /etc/nginx/snippets/zareian-front-headers.conf;
|
||||
add_header Cache-Control "public, max-age=31536000, immutable" always;
|
||||
}
|
||||
|
||||
location @fallback {
|
||||
rewrite ^ /index.html break;
|
||||
}
|
||||
}
|
||||
22
ops/nginx/zareian-arvan-only.conf
Normal file
22
ops/nginx/zareian-arvan-only.conf
Normal file
@@ -0,0 +1,22 @@
|
||||
# روی ۲۴۹: /etc/nginx/snippets/zareian-arvan-only.conf — داخلِ server {}ِ دو سایتِ ما include میشود.
|
||||
# فقط edgeهای ArvanCloud (و خودِ ماشین) به اپ میرسند؛ درخواستِ مستقیم به IPِ سرور ۴۰۳ میگیرد، پس سپرِ
|
||||
# DDoS/WAFِ آروان دور زده نمیشود. فهرست از https://www.arvancloud.ir/fa/ips.txt (۲۰۲۶-۱۰-۱۳)؛ همان فهرستِ
|
||||
# TRUSTED_PROXY_NETWORKS در config/settings.py. اگر آروان رنجِ تازه بگیرد و اینجا نباشد، کاربرانِ پشتِ آن edge
|
||||
# ۴۰۳ میگیرند (در /var/log/nginx/access.log دیده میشود): هر دو فهرست را با هم تازه کنید.
|
||||
allow 127.0.0.1;
|
||||
allow ::1;
|
||||
allow 185.143.232.0/22;
|
||||
allow 188.229.116.16/30;
|
||||
allow 94.101.182.0/27;
|
||||
allow 2.144.3.128/28;
|
||||
allow 37.32.16.0/27;
|
||||
allow 37.32.17.0/27;
|
||||
allow 37.32.18.0/27;
|
||||
allow 37.32.19.0/27;
|
||||
allow 185.215.232.0/22;
|
||||
allow 178.131.120.48/28;
|
||||
allow 94.101.183.0/28;
|
||||
allow 78.157.36.112/28;
|
||||
allow 95.38.61.80/28;
|
||||
allow 193.24.119.0/29;
|
||||
deny all;
|
||||
17
ops/nginx/zareian-front-headers.conf
Normal file
17
ops/nginx/zareian-front-headers.conf
Normal file
@@ -0,0 +1,17 @@
|
||||
# روی ۲۴۹: /etc/nginx/snippets/zareian-front-headers.conf — هدرهای امنیتیِ math.tadrisino2.ir.
|
||||
# در هر locationی که خودش add_header دارد دوباره include شود: nginx هدرهای سطحِ server را در چنین locationی ارث نمیدهد.
|
||||
#
|
||||
# CSP فقط منابعِ خودمان را میپذیرد:
|
||||
# - script: فایلهای خودِ سایت، و تنها اسکریپتِ درونخطیِ index.html (کلاسِ in-app برای اسپلش) با هشش. اگر آن
|
||||
# اسکریپت در فرانت عوض شود، هش را از نو بسازید (python: base64(sha256(متنِ بینِ <script> و </script>)))؛
|
||||
# وگرنه فقط همان اسکریپت اجرا نمیشود و کنسول «Refused to execute inline script» میگوید.
|
||||
# - connect: API. با عوض شدنِ دامنهی API (config.js) اینجا هم عوض شود (docs/operations.md «عوض کردنِ دامنه»).
|
||||
# - worker blob: کتابخانهی confetti یک Worker از blob میسازد.
|
||||
# - style 'unsafe-inline': <style>ِ index.html و pay.html و استایلِ درونخطیِ React.
|
||||
# HSTS بی includeSubDomains: زیردامنههای دیگرِ tadrisino2.ir مالِ تیمهای دیگرند.
|
||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-utBc36Q6M7NacvbHQCaMGCgm3osZ627xGQGm/tnSEXM='; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self' data:; connect-src 'self' https://mathapi.tadrisino2.ir; worker-src 'self' blob:; media-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=(), usb=()" always;
|
||||
add_header Strict-Transport-Security "max-age=31536000" always;
|
||||
Reference in New Issue
Block a user