From bc04162a31f0654c35e047c11384d1f3c6bc4755 Mon Sep 17 00:00:00 2001 From: lokses Date: Tue, 22 Sep 2026 13:00:31 +0300 Subject: [PATCH] =?UTF-8?q?chat:=20=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20CSRF?= =?UTF-8?q?=20=D0=B8=D0=B7=20hx-headers=20=E2=80=94=20HttpOnly=20cookie=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=BC=D0=B0=D0=BB=D0=B0=20POST=20/chat/api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Иначе Django отдавал 403 на /users/ids, posts, channels/direct и т.п. Co-authored-by: Cursor --- apps/mattermost_connector/tests.py | 38 ++++++++++++++++++++++++++++++ static/assets/js/chat.js | 13 ++++++++++ 2 files changed, 51 insertions(+) diff --git a/apps/mattermost_connector/tests.py b/apps/mattermost_connector/tests.py index 7661388..6afb93f 100755 --- a/apps/mattermost_connector/tests.py +++ b/apps/mattermost_connector/tests.py @@ -391,3 +391,41 @@ class ChatApiProxyTests(TestCase): def test_rejects_path_traversal(self): resp = self.client.get('/chat/api/users/../admin') self.assertEqual(resp.status_code, 400) + + @mock.patch('apps.mattermost_connector.chat_proxy.httpx.Client') + def test_post_users_ids_requires_csrf(self, client_cls): + """POST без CSRF → 403 (как в проде при HttpOnly cookie + пустой X-CSRFToken).""" + from django.test import Client + + csrf_client = Client(enforce_csrf_checks=True) + from core.test_utils import login_2fa + login_2fa(csrf_client, self.user) + + resp = csrf_client.post( + '/chat/api/users/ids', + data='["mm-proxy"]', + content_type='application/json', + ) + self.assertEqual(resp.status_code, 403) + + upstream = mock.Mock() + upstream.status_code = 200 + upstream.content = b'[{"id":"mm-proxy","username":"proxy"}]' + upstream.headers = {'content-type': 'application/json'} + client = mock.MagicMock() + client.__enter__.return_value = client + client.__exit__.return_value = False + client.request.return_value = upstream + client_cls.return_value = client + + csrf_client.get('/chat/') # ensure csrf cookie + token = csrf_client.cookies.get('csrftoken') + self.assertIsNotNone(token) + resp_ok = csrf_client.post( + '/chat/api/users/ids', + data='["mm-proxy"]', + content_type='application/json', + HTTP_X_CSRFTOKEN=token.value, + ) + self.assertEqual(resp_ok.status_code, 200) + self.assertIn(b'mm-proxy', resp_ok.content) diff --git a/static/assets/js/chat.js b/static/assets/js/chat.js index bfbbd9f..d643db0 100644 --- a/static/assets/js/chat.js +++ b/static/assets/js/chat.js @@ -174,6 +174,19 @@ function chatOpenDmWithUser(userId) { } function chatCsrfToken() { + // CSRF_COOKIE_HTTPONLY=True — cookie из JS не видна; как у HTMX берём из body hx-headers. + var body = document.body; + if (body) { + var hx = body.getAttribute('hx-headers'); + if (hx) { + try { + var parsed = JSON.parse(hx); + if (parsed['X-CSRFToken']) return parsed['X-CSRFToken']; + } catch (e) { /* ignore */ } + } + } + var input = document.querySelector('input[name=csrfmiddlewaretoken]'); + if (input && input.value) return input.value; var m = document.cookie.match(/(?:^|; )csrftoken=([^;]*)/); return m ? decodeURIComponent(m[1]) : ''; }