From 028cc2446f54b766b84b0d1e1db4237b1699a3ae Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Tue, 14 Apr 2026 19:23:21 -0700 Subject: [PATCH] Don't allow cookies with Authorization: Bearer to bypass CSRF Refs #2689 --- datasette/csrf.py | 5 ++++- tests/test_csrf_middleware.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/datasette/csrf.py b/datasette/csrf.py index dd968a4d..845c8fb4 100644 --- a/datasette/csrf.py +++ b/datasette/csrf.py @@ -68,7 +68,10 @@ class CrossOriginProtectionMiddleware: # before evaluating Sec-Fetch-Site / Origin. Only "Bearer" is exempt; # schemes like Basic or Digest can be browser-managed and ambient. authorization = headers.get(b"authorization", b"").decode("latin-1") - if authorization: + cookie_header = headers.get(b"cookie") + # If the request also carries a Cookie header, ambient cookie auth + # could be in play, so do NOT treat it as exempt. + if authorization and not cookie_header: scheme = authorization.split(None, 1)[0].lower() if scheme == "bearer": await self.app(scope, receive, send) diff --git a/tests/test_csrf_middleware.py b/tests/test_csrf_middleware.py index 07ff598e..820df1e7 100644 --- a/tests/test_csrf_middleware.py +++ b/tests/test_csrf_middleware.py @@ -252,6 +252,20 @@ async def test_cross_site_post_without_auth_still_blocked(bare_ds): assert response.status_code == 403 +@pytest.mark.asyncio +async def test_bearer_with_cookie_does_not_bypass(): + # Bearer + Cookie => ambient cookie auth is in play, not exempt. + scope = _http_scope( + { + "sec-fetch-site": "cross-site", + "host": "example.com", + "authorization": "Bearer dstok_abc", + "cookie": "ds_actor=anything", + } + ) + assert await _run_middleware(scope) == ("blocked", 403) + + def test_legacy_skip_csrf_hookimpl_does_not_break_loading(): # Plugins that still define skip_csrf must load cleanly - pluggy ignores # unknown hook implementations - even though the hook is no longer