Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions tests/test_security_csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ async def scenario():
_run(scenario())


def test_csrf_cookie_is_not_httponly():
"""The double-submit pattern requires client-side JS to read this cookie
and echo it back as a header; HttpOnly would make that impossible,
silently breaking CSRF protection for every real browser client."""
app = _app_with_csrf()

async def scenario():
async with TestClient(app) as client:
resp = await client.get("/page")
assert "httponly" not in resp.headers["set-cookie"].lower()

_run(scenario())


def test_get_with_existing_valid_cookie_does_not_reset():
app = _app_with_csrf()

Expand Down
13 changes: 10 additions & 3 deletions velocix/security/csrf.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,16 +141,23 @@ def _validate_token(self, token: str) -> bool:
return False

def _set_csrf_cookie(self, response: Response, token: str) -> None:
"""Set the CSRF cookie on the response."""
"""Set the CSRF cookie on the response.

Deliberately NOT HttpOnly: the double-submit pattern requires
client-side JavaScript to read this cookie and echo its value back
in the request header (self._header_name) on state-changing
requests -- that's what proves the request came from same-origin
script rather than a cross-site form/link. An HttpOnly cookie here
can never be read by that script, which defeats the entire scheme.
"""
response.raw_headers.append((
b"set-cookie",
(
f"{self._cookie_name}={token}; "
f"Path={self._cookie_path}; "
f"SameSite={self._cookie_samesite}; "
f"{'Secure; ' if self._cookie_secure else ''}"
f"HttpOnly"
).encode("latin-1"),
).rstrip("; ").encode("latin-1"),
))

async def _on_request(self, request: Request) -> Response:
Expand Down
Loading