stop counting the frontend developer's port numbers
فهرستِ ثابتِ CORS (۳۰۰۰ و ۵۱۷۳) روزی شکست که Vite دید ۵۱۷۳ اشغال است و بیسروصدا رفت روی ۵۱۷۴ — رفتارِ پیشفرضِ خودش. فرانتکار خطای CORS گرفت، از سمتِ سرور هیچ ردی در هیچ لاگی نبود (خطا فقط در کنسولِ مرورگر دیده میشود)، و چون همزمان با تغییراتِ بکاند شد به اشتباه گردنِ ما افتاد. هزینهاش یک ویرایشِ .env و یک ریاستارتِ پروداکشن بود، برای چیزی که اصلاً به پروداکشن ربطی نداشت. بهجای شمردنِ پورتها، الگوی «هر پورتی روی خودِ ماشینِ توسعهدهنده» پذیرفته شد. سطحِ حمله اضافه نمیکند: توکنِ ما در هدر است نه کوکی و CORS_ALLOW_CREDENTIALS خاموش، پس صفحهای روی localhostِ مهاجم هیچ اعتبارنامهای از مرورگرِ قربانی نمیگیرد؛ CORS هم اصلاً سدِ سرور-به-سرور نیست. تستها هم پوشش میدهند که localhost.evil.com و math.tadrisino2.ir.evil.com همچنان رد شوند. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -134,10 +134,25 @@ SESSION_COOKIE_HTTPONLY = True
|
||||
# در کنسولِ مرورگر دیده میشود.
|
||||
CORS_DEFAULT_ORIGINS = [
|
||||
'https://math.tadrisino2.ir', # فرانتِ مستقر
|
||||
'http://localhost:3000', 'http://127.0.0.1:3000',
|
||||
'http://localhost:5173', 'http://127.0.0.1:5173', # Vite/CRA محلی
|
||||
]
|
||||
CORS_ALLOWED_ORIGINS = _with_defaults('CORS_ALLOWED_ORIGINS', CORS_DEFAULT_ORIGINS)
|
||||
|
||||
# ⚠️ پورتِ سرورِ توسعه را نباید فهرست کرد. فهرستِ ثابتِ قبلی (۳۰۰۰ و ۵۱۷۳) روزی
|
||||
# شکست که Vite دید ۵۱۷۳ اشغال است و بیسروصدا رفت روی ۵۱۷۴ — رفتارِ پیشفرضِ
|
||||
# خودش. آنوقت فرانتکار خطای CORS گرفت، از سمتِ سرور هیچ ردی در هیچ لاگی
|
||||
# نبود، و چون همزمان با تغییراتِ ما شد، به اشتباه گردنِ بکاند افتاد.
|
||||
# هر بار که این تکرار شود یعنی یک ویرایشِ .env و یک ریاستارتِ پروداکشن برای
|
||||
# چیزی که اصلاً به پروداکشن ربطی ندارد.
|
||||
#
|
||||
# پس بهجای شمردنِ پورتها، الگوی «هر پورتی روی خودِ ماشینِ توسعهدهنده» را
|
||||
# میپذیریم. این سطحِ حمله اضافه نمیکند: احرازِ هویتِ ما توکنِ Bearer در هدر
|
||||
# است و `CORS_ALLOW_CREDENTIALS` خاموش، پس صفحهای که روی localhostِ یک مهاجم
|
||||
# باز است هیچ اعتبارنامهای از مرورگرِ قربانی به دست نمیآورد؛ CORS هم اصلاً
|
||||
# سدِ سرور-به-سرور نیست.
|
||||
CORS_ALLOWED_ORIGIN_REGEXES = [
|
||||
r'^http://localhost:\d+$',
|
||||
r'^http://127\.0\.0\.1:\d+$',
|
||||
]
|
||||
CORS_ALLOW_CREDENTIALS = False
|
||||
# فقط مسیرهای API؛ /admin/ و فایلهای استاتیک لازم نیست از origin دیگر خوانده شوند.
|
||||
CORS_URLS_REGEX = r'^/api/.*$'
|
||||
|
||||
@@ -403,3 +403,51 @@ class SecureCookieTests(SimpleTestCase):
|
||||
"""بدونِ این، کوکیِ امن پشتِ nginx هرگز ست نمیشود و ورود میشکند."""
|
||||
self.assertEqual(settings.SECURE_PROXY_SSL_HEADER,
|
||||
('HTTP_X_FORWARDED_PROTO', 'https'))
|
||||
|
||||
|
||||
class CorsDevPortTests(TestCase):
|
||||
"""
|
||||
پورتِ سرورِ توسعه نباید در فهرستِ CORS شمرده شود.
|
||||
|
||||
فهرستِ ثابتِ قبلی (۳۰۰۰ و ۵۱۷۳) روزی شکست که Vite دید ۵۱۷۳ اشغال است و
|
||||
بیسروصدا رفت روی ۵۱۷۴. فرانتکار خطای CORS گرفت، از سمتِ سرور هیچ ردی در
|
||||
هیچ لاگی نبود، و چون همزمان با تغییراتِ بکاند شد به اشتباه گردنِ ما افتاد.
|
||||
هزینهاش یک ویرایشِ .env و یک ریاستارتِ پروداکشن بود، برای چیزی که اصلاً
|
||||
به پروداکشن ربطی نداشت.
|
||||
"""
|
||||
|
||||
def _preflight(self, origin):
|
||||
return self.client.options(
|
||||
'/api/grades',
|
||||
HTTP_ORIGIN=origin,
|
||||
HTTP_ACCESS_CONTROL_REQUEST_METHOD='GET',
|
||||
)
|
||||
|
||||
def test_any_localhost_port_is_allowed_so_a_new_dev_port_never_breaks_again(self):
|
||||
for origin in ('http://localhost:5173', 'http://localhost:5174',
|
||||
'http://localhost:3000', 'http://127.0.0.1:8080'):
|
||||
with self.subTest(origin=origin):
|
||||
resp = self._preflight(origin)
|
||||
self.assertEqual(
|
||||
resp.headers.get('access-control-allow-origin'), origin)
|
||||
|
||||
def test_the_deployed_frontend_origin_still_works(self):
|
||||
resp = self._preflight('https://math.tadrisino2.ir')
|
||||
self.assertEqual(resp.headers.get('access-control-allow-origin'),
|
||||
'https://math.tadrisino2.ir')
|
||||
|
||||
def test_a_stranger_origin_is_still_refused(self):
|
||||
# الگو فقط localhost است، نه هر دامنهای؛ و «localhost» بهعنوانِ
|
||||
# زیررشته در دامنهی دیگری نباید رد شود.
|
||||
for origin in ('https://evil.example.com',
|
||||
'http://localhost.evil.com',
|
||||
'https://math.tadrisino2.ir.evil.com'):
|
||||
with self.subTest(origin=origin):
|
||||
resp = self._preflight(origin)
|
||||
self.assertIsNone(resp.headers.get('access-control-allow-origin'))
|
||||
|
||||
def test_credentials_stay_off(self):
|
||||
# توکنِ ما در هدر است نه کوکی؛ روشن کردنِ این فقط سطحِ حمله اضافه میکند
|
||||
# و پذیرشِ هر پورتِ localhost را واقعاً خطرناک میکرد.
|
||||
from django.conf import settings
|
||||
self.assertFalse(settings.CORS_ALLOW_CREDENTIALS)
|
||||
|
||||
Reference in New Issue
Block a user