@@ -253,6 +253,52 @@ async def noop_app(scope, receive, send):
253253 # user1_token should not validate for user_id=2
254254 assert not mw ._csrf_tokens_match (user1_token , user1_token , user_id = 2 )
255255
256+ def test_stale_user_token_accepted_when_anonymous (self ) -> None :
257+ """A token left over from a dead session must still authorise an
258+ anonymous request.
259+
260+ Regression test for the "had to log in twice" bug: the browser keeps a
261+ CSRF cookie bound to user N, the server-side session is gone (restart /
262+ expiry), so the login POST is anonymous. Rejecting it here failed the
263+ first attempt and rotated the cookie, letting the retry through.
264+ Double-submit still protects this case, since an attacker can't read
265+ the cookie to forge the matching header.
266+ """
267+
268+ async def noop_app (scope , receive , send ):
269+ pass
270+
271+ mw = CSRFMiddleware (app = noop_app , secret = "test" )
272+ stale_token = mw ._generate_csrf_token (user_id = 7 )
273+
274+ assert mw ._csrf_tokens_match (stale_token , stale_token , user_id = None )
275+
276+ def test_anonymous_still_requires_cookie_and_header_to_match (self ) -> None :
277+ """Relaxing the user binding must not relax double-submit itself."""
278+
279+ async def noop_app (scope , receive , send ):
280+ pass
281+
282+ mw = CSRFMiddleware (app = noop_app , secret = "test" )
283+ token_a = mw ._generate_csrf_token (user_id = None )
284+ token_b = mw ._generate_csrf_token (user_id = None )
285+
286+ assert not mw ._csrf_tokens_match (token_a , token_b , user_id = None )
287+
288+ def test_authenticated_user_binding_still_enforced (self ) -> None :
289+ """The security property: a signed-in caller can't use another user's
290+ token, even though the anonymous case is now lenient."""
291+
292+ async def noop_app (scope , receive , send ):
293+ pass
294+
295+ mw = CSRFMiddleware (app = noop_app , secret = "test" )
296+ other_user_token = mw ._generate_csrf_token (user_id = 1 )
297+ anonymous_token = mw ._generate_csrf_token (user_id = None )
298+
299+ assert not mw ._csrf_tokens_match (other_user_token , other_user_token , user_id = 2 )
300+ assert not mw ._csrf_tokens_match (anonymous_token , anonymous_token , user_id = 2 )
301+
256302 def test_post_with_mismatched_but_valid_tokens_fails (self ) -> None :
257303 """POST with a valid header token that doesn't match the cookie token should fail."""
258304 app = create_test_app ()
0 commit comments