From 77d662a9cbdb326af24cc445d6c3eff20bcccf39 Mon Sep 17 00:00:00 2001 From: mrwnslz Date: Tue, 29 Sep 2026 19:33:18 +0200 Subject: [PATCH] API-Token: Anfragen mit Bearer-Token brauchen kein CSRF-Token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Native Clients (diora desktop) haben kein CSRF-Cookie; ein Browser hängt den Authorization-Header nie von selbst an, daher ist das sicher. Nötig z. B. für den M3U-Import, der nicht csrf_exempt ist. Co-Authored-By: Claude Opus 5.5 --- accounts/middleware.py | 5 +++++ accounts/tests.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/accounts/middleware.py b/accounts/middleware.py index 3ceb447..addb990 100644 --- a/accounts/middleware.py +++ b/accounts/middleware.py @@ -22,4 +22,9 @@ class ApiTokenAuthMiddleware: api_token = ApiToken.objects.select_related('user').filter(token=token).first() if api_token is not None: request.user = api_token.user + # Browsers never attach this header on their own, so a + # token request can't be a cross-site forgery; native + # clients have no CSRF cookie to send. (CsrfViewMiddleware + # checks this flag in process_view, which runs after us.) + request._dont_enforce_csrf_checks = True return self.get_response(request) diff --git a/accounts/tests.py b/accounts/tests.py index 4d4e05a..638ec51 100644 --- a/accounts/tests.py +++ b/accounts/tests.py @@ -42,6 +42,20 @@ class ApiTokenAuthMiddlewareTests(TestCase): resp = self.client.get('/books/', HTTP_AUTHORIZATION=f'Bearer {self.token.token}') self.assertEqual(resp.status_code, 200) + def test_bearer_token_needs_no_csrf_token(self): + # import_m3u is not csrf_exempt: without a token it must still be protected. + from django.test import Client + client = Client(enforce_csrf_checks=True) + m3u = '#EXTM3U\n#EXTINF:-1,Test FM\nhttps://example.com/stream\n' + resp = client.post('/radio/import/', data=m3u, content_type='text/plain') + self.assertIn(resp.status_code, (302, 401, 403)) + resp = client.post('/radio/import/', data=m3u, content_type='text/plain', + HTTP_AUTHORIZATION=f'Bearer {self.token.token}') + self.assertNotEqual(resp.status_code, 403) + resp = client.post('/radio/import/', data=m3u, content_type='text/plain', + HTTP_AUTHORIZATION='Bearer not-a-real-token') + self.assertIn(resp.status_code, (302, 401, 403)) + def test_regenerate_view_requires_login_and_rotates_token(self): resp = self.client.post('/accounts/api-token/regenerate/') self.assertEqual(resp.status_code, 302)