From a67ab33e5db5c7b3777152d3b90fc368d02c3ecc Mon Sep 17 00:00:00 2001 From: emelinda Date: Mon, 10 Aug 2026 13:25:06 +0300 Subject: [PATCH] =?UTF-8?q?Django:=20DELETE=20=D0=BF=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=B2=20403=20=E2=80=94=20=D1=81=D0=B5=D1=81=D1=81?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B1=D0=B5=D0=B7=20X-CSRFToken?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SPA аутентифицируется сессионной кукой и не прикладывает X-CSRFToken, а SessionAuthentication из DRF требует его на любом небезопасном методе: DELETE /api/core/videos// -> 403 {"detail": "CSRF Failed: CSRF token missing."} Проверено на контуре, тремя запросами по несуществующему id (403 = отказ авторизации, 404 = авторизация прошла): Bearer-токен -> 404 CSRF не при чём сессия + X-CSRFToken -> 404 проходит сессия без X-CSRFToken -> 403 ровно наша ошибка без авторизации -> 401 То есть бэкенд исправен, ломается ровно сочетание "кука без заголовка". С Bearer до SessionAuthentication дело не доходит: simple_jwt стоит в DEFAULT_AUTHENTICATION_CLASSES раньше. Ставим подкласс с пустым enforce_csrf на то же пятое место. Цена — с cookie-запросов снимается защита от CSRF. Основной вектор закрыт самими куками (sessionid и csrftoken выставляются с SameSite=Lax, браузер не пошлёт их при кросс-сайтовом DELETE); остаётся щель в соседях по домену, поскольку SameSite смотрит на site, а не на origin. Импорт rest_framework в модуле настроек безопасен не всегда — обычно так ловят AppRegistryNotReady. Здесь проверено запуском django.setup() внутри работающего пода backend: setup проходит, строка пути к классу резолвится. --- apps/django/aero/django-configmap.yaml | 43 ++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/apps/django/aero/django-configmap.yaml b/apps/django/aero/django-configmap.yaml index 11153ad..bcd2711 100644 --- a/apps/django/aero/django-configmap.yaml +++ b/apps/django/aero/django-configmap.yaml @@ -4,7 +4,7 @@ # (331 строка python), kustomize умеет заменить его только целиком. Ровно та же # причина, что у nginx-configmap.yaml рядом. # -# ЧТО ИЗМЕНЕНО — две строки, обе про домен sarex.contour.infra.sarex.tech, +# ЧТО ИЗМЕНЕНО — две строки про домен sarex.contour.infra.sarex.tech, # принадлежащий ДРУГОМУ контуру: # # HOST — по нему backend строит АБСОЛЮТНЫЕ ссылки на медиа. @@ -14,6 +14,11 @@ # CSRF_TRUSTED_ORIGINS — без нашего домена django отклонял бы POST-формы # админки с ошибкой проверки Referer. # +# И один блок сверх того: CsrfExemptSessionAuthentication в +# DEFAULT_AUTHENTICATION_CLASSES вместо штатного SessionAuthentication — +# из-за него SPA получала 403 на любое удаление. Обоснование и цена решения +# расписаны прямо над классом. +# # Больше в файле ничего не менялось: сравнивать с base удобно обычным diff. apiVersion: v1 kind: ConfigMap @@ -215,6 +220,40 @@ data: }, } + # Сессионная аутентификация без проверки CSRF. + # + # SPA платформы аутентифицируется сессионной кукой и не прикладывает + # заголовок X-CSRFToken, а SessionAuthentication из DRF требует его на + # любом небезопасном методе. Наружу это выглядело так: + # DELETE /api/core/videos// → 403 + # {"detail": "CSRF Failed: CSRF token missing."} + # + # Проверено на контуре: тот же запрос с X-CSRFToken проходит, и он же с + # заголовком Authorization: Bearer проходит тоже — до SessionAuthentication + # дело в этом случае просто не доходит, потому что simple_jwt в списке ниже + # стоит раньше. То есть бэкенд исправен, чинить нужно ровно сочетание + # "кука без заголовка". + # + # ЧЕМ ПЛАТИМ: с запросов, авторизованных кукой, снимается защита от CSRF. + # Основной вектор закрыт самими куками — sessionid и csrftoken выставляются + # с SameSite=Lax, и браузер не пошлёт их при кросс-сайтовом DELETE. Щель + # остаётся одна: SameSite смотрит на site, а не на origin, поэтому соседи + # по домену (minio., rabbitmq., dashboard.) считаются "своими", а в MinIO + # лежит пользовательский контент. + # + # Импорт DRF прямо в модуле настроек безопасен НЕ всегда — обычно так и + # ловят AppRegistryNotReady. Здесь проверено запуском django.setup() внутри + # работающего пода: rest_framework.authentication тянет только + # django.contrib.auth и rest_framework.exceptions, и ни один из них к + # моделям на уровне модуля не обращается. + from rest_framework.authentication import SessionAuthentication + + + class CsrfExemptSessionAuthentication(SessionAuthentication): + def enforce_csrf(self, request): + return + + REST_FRAMEWORK = { 'DEFAULT_PAGINATION_CLASS': ( 'rest_framework.pagination.LimitOffsetPagination' ), 'DEFAULT_SCHEMA_CLASS': 'rest_framework.schemas.coreapi.AutoSchema', @@ -225,7 +264,7 @@ data: 'rest_framework.authentication.RemoteUserAuthentication', 'rest_framework_simplejwt.authentication.JWTAuthentication', 'rest_framework.authentication.BasicAuthentication', - 'rest_framework.authentication.SessionAuthentication', + 'config.settings.production.CsrfExemptSessionAuthentication', 'sarex.authentication.backends.JWTAuthentication' ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticated', ] }