No CSRF protection beyond SameSite=Lax #62
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: HIGH
The bug
Sessions are cookie-based and ambient, and the only CSRF defence is
samesite="lax"(
backend/app/auth/session.py:31). There is no CSRF token, no origin check, and nocustom-header requirement — confirmed by grepping the backend.
SameSite=Laxblocks cross-site form POSTs, which covers the obvious case. Two gaps remain:Laxis same-site, not same-origin. Any other service on the same registrable domainis same-site — a common shape in self-hosted setups where several apps sit behind one domain.
A compromised or XSS-bearing sibling app can issue credentialed
fetchcalls to Circa.allow_origins=["http://localhost:5173"]with
allow_credentials=Truegives any page served from that origin full authenticated APIaccess.
Impact
The realistic outcome is quiet integrity damage: mass-approving photos with wrong dates, or
wiping notes. There is no second copy of any of it.
Fix
samesite="strict"for the session cookie. The OAuth callback lands on the backend originand sets the cookie there, so no cross-site entry flow needs
Lax.Origin/Referercheck on all mutating requests, rejecting anything whose origin isnot the configured app origin.
everything through one fetch wrapper where a header can be added centrally.
Done when
SameSite=StrictOriginare rejectedReferences
backend/app/auth/session.py:31backend/app/main.py(middleware stack; no CSRF middleware)Fixed in
791d117. CI green.Two layers, in
app/middleware.py:Session cookie moved to
SameSite=strict. Nothing in the app relies on the cookie being sent ona cross-site top-level navigation — the OIDC callback lands on the backend origin and sets the
cookie there — so the usual reason to prefer
laxdoes not apply.An explicit origin check on every unsafe method. This is the part that closes the gap in the
issue:
SameSiteis a same-site control, so a sibling host on the same registrable domain istreated as first-party, which is a realistic shape in self-hosted deployments running several
services behind one domain.
It fails closed — a state-changing request with no
Originand noRefereris refused,because browsers send
Originon exactly these requests.Refereris used as a fallback since someprivacy configurations strip
Origin./api/auth/callbackand/api/auth/loginare exempt: the provider redirects the browser back as atop-level navigation with no
Origin, so checking there would break login.Tested: foreign origin refused, missing origin refused, app origin passes through to the handler,
and a sibling subdomain refused — the case
SameSitealone would have allowed.