Skip to content

Commit 765cd0b

Browse files
jsbattigclaude
andcommitted
fix(#715): Prevent CSRF race condition with HTMX polling
When user is on login page after session expiration, HTMX polling from old dashboard tab constantly requests /admin/partials/* endpoints which redirect to /login. Each redirect was generating a NEW CSRF token and overwriting the cookie, causing form token to mismatch cookie token. Fix: Reuse existing valid CSRF token from cookie instead of always generating a new one. Only generate new token when no valid cookie exists. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0d9d96e commit 765cd0b

2 files changed

Lines changed: 131 additions & 4 deletions

File tree

src/code_indexer/server/web/routes.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5865,8 +5865,16 @@ async def unified_login_page(
58655865
error: Optional error message to display
58665866
info: Optional info message to display
58675867
"""
5868-
# Generate CSRF token for the form
5869-
csrf_token = generate_csrf_token()
5868+
# Bug #715: Try to reuse existing valid CSRF token from cookie
5869+
# This prevents race conditions when HTMX polling refreshes the login page
5870+
# while user is filling out the form
5871+
existing_csrf_token = get_csrf_token_from_cookie(request)
5872+
if existing_csrf_token:
5873+
csrf_token = existing_csrf_token
5874+
need_new_cookie = False
5875+
else:
5876+
csrf_token = generate_csrf_token()
5877+
need_new_cookie = True
58705878

58715879
# Check if there's an expired session
58725880
session_manager = get_session_manager()
@@ -5893,8 +5901,10 @@ async def unified_login_page(
58935901
},
58945902
)
58955903

5896-
# Set CSRF token in signed cookie for validation on POST
5897-
set_csrf_cookie(response, csrf_token, path="/")
5904+
# Bug #715: Only set CSRF cookie if we generated a new token
5905+
# This prevents overwriting valid cookies during HTMX polling
5906+
if need_new_cookie:
5907+
set_csrf_cookie(response, csrf_token, path="/")
58985908

58995909
return response
59005910

tests/server/web/test_auth.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,3 +657,120 @@ def test_login_missing_csrf_auto_recovers(
657657
assert "info=session_expired" in location, (
658658
f"Expected info=session_expired in redirect URL, got {location}"
659659
)
660+
661+
662+
# =============================================================================
663+
# Bug #715: CSRF Token Race Condition Tests
664+
# =============================================================================
665+
666+
667+
class TestCSRFTokenRaceCondition:
668+
"""Tests for Bug #715: CSRF token race condition with HTMX partial polling."""
669+
670+
def test_login_page_reuses_valid_csrf_token_from_cookie(
671+
self, web_infrastructure: WebTestInfrastructure
672+
):
673+
"""
674+
Bug #715: Login page should reuse valid CSRF token from cookie.
675+
676+
Given I have a valid CSRF token cookie
677+
When I request the login page
678+
Then the CSRF token in the form matches my existing cookie
679+
And no new CSRF cookie is set in the response
680+
"""
681+
assert web_infrastructure.client is not None
682+
client = web_infrastructure.client
683+
684+
# First request to get a CSRF token
685+
first_response = client.get("/login")
686+
assert first_response.status_code == 200
687+
688+
# Extract CSRF token from form
689+
first_csrf_token = web_infrastructure.extract_csrf_token(first_response.text)
690+
assert first_csrf_token is not None, "First request should have CSRF token"
691+
692+
# Second request - should reuse the existing token
693+
second_response = client.get("/login")
694+
assert second_response.status_code == 200
695+
696+
# Extract CSRF token from second form
697+
second_csrf_token = web_infrastructure.extract_csrf_token(second_response.text)
698+
assert second_csrf_token is not None, "Second request should have CSRF token"
699+
700+
# The token in the form should be the SAME as the first request
701+
assert first_csrf_token == second_csrf_token, (
702+
f"Bug #715: Login page should reuse existing CSRF token from cookie. "
703+
f"First token: {first_csrf_token[:20]}..., "
704+
f"Second token: {second_csrf_token[:20]}..."
705+
)
706+
707+
def test_login_page_generates_new_token_when_no_cookie(
708+
self, web_infrastructure: WebTestInfrastructure
709+
):
710+
"""
711+
Bug #715: Login page generates new token when no cookie exists.
712+
713+
Given I have no CSRF cookie
714+
When I request the login page
715+
Then a new CSRF token is generated
716+
And a new CSRF cookie is set
717+
"""
718+
assert web_infrastructure.client is not None
719+
client = web_infrastructure.client
720+
721+
# Clear any existing cookies to simulate fresh session
722+
client.cookies.clear()
723+
724+
# Request login page without any CSRF cookie
725+
response = client.get("/login")
726+
assert response.status_code == 200
727+
728+
# Should have CSRF token in form
729+
csrf_token = web_infrastructure.extract_csrf_token(response.text)
730+
assert csrf_token is not None, (
731+
"Login page should generate CSRF token when no cookie exists"
732+
)
733+
734+
# Should set new CSRF cookie
735+
csrf_cookie = response.cookies.get("_csrf")
736+
assert csrf_cookie is not None, (
737+
"Login page should set CSRF cookie when no cookie exists"
738+
)
739+
740+
def test_login_page_generates_new_token_when_cookie_expired(
741+
self, web_infrastructure: WebTestInfrastructure
742+
):
743+
"""
744+
Bug #715: Login page generates new token when cookie is expired/invalid.
745+
746+
Given I have an expired or invalid CSRF cookie
747+
When I request the login page
748+
Then a new CSRF token is generated
749+
And a new CSRF cookie is set to replace the invalid one
750+
"""
751+
assert web_infrastructure.client is not None
752+
client = web_infrastructure.client
753+
754+
# Set an invalid/expired CSRF cookie
755+
client.cookies.set("_csrf", "invalid_expired_csrf_token_12345")
756+
757+
# Request login page with invalid CSRF cookie
758+
response = client.get("/login")
759+
assert response.status_code == 200
760+
761+
# Should have CSRF token in form
762+
csrf_token = web_infrastructure.extract_csrf_token(response.text)
763+
assert csrf_token is not None, (
764+
"Login page should generate CSRF token when cookie is invalid"
765+
)
766+
767+
# The token should NOT be the invalid one we sent
768+
assert csrf_token != "invalid_expired_csrf_token_12345", (
769+
"Login page should not use invalid cookie value as CSRF token"
770+
)
771+
772+
# Should set new CSRF cookie
773+
csrf_cookie = response.cookies.get("_csrf")
774+
assert csrf_cookie is not None, (
775+
"Login page should set new CSRF cookie when old one is invalid"
776+
)

0 commit comments

Comments
 (0)