fix(web): Origin / Referer CSRF defense on every state-changing POST #291

Merged
YousifShkara merged 1 commit from fix/BUNYIP-259-csrf-synchronizer-token into main 2026-06-30 07:05:12 +02:00
Owner

BUNYIP-259: every state-changing POST on bunyip-web now refuses cross-origin form submissions before the handler runs. The audit's full spec is a per-session synchronizer-token middleware plus a hidden form input on every <form> (~25 templates), which is the right end-state but a real regression risk for the OIDC redirect chain. Ship the smaller fully-defensive step first: an Origin / Referer middleware that compares the submission's source host against the request's own Host header.

What this DOES catch:

  • A cross-origin form on any other host POSTing to bunyip-web. Browsers send Origin reliably on cross-site POSTs; the mismatch trips the middleware before the auth handler runs.
  • An attacker-controlled child app under *.{app_domain} submitting to bunyip-web. CSP's form-action was widened by BUNYIP-249/271 to include those origins, so this is the layer that closes the surface that opening created. The child app's Origin is on the child host, not the BFF host.

What this does NOT catch:

  • A same-origin XSS that synthesizes a POST. CSP is the layer that stops that one; this middleware can't tell legit JS from injected JS within the same origin.
  • A browser that strips both Origin and Referer for some misconfigured reason. The middleware fails closed (403); the user retries from a normal-config browser. The error body names the cause so users understand why a request was refused.

Exemptions: the /oauth2/* family is exempt because the spec gates those POSTs on PKCE + state + nonce + client authentication. Cross-origin OIDC flows are exactly what the spec expects an OP to accept; refusing them here would break every legitimate RP integration. The Stripe webhook lives on bunyip-api, not bunyip-web, so it's not affected.

Follow-up: a synchronizer-token middleware on top of this is its own ticket. It's defense in depth on top of the Origin check, not a substitute. The token threading needs every <form> template to render a hidden input and is heavier than this PR's scope.

#BUNYIP-259

BUNYIP-259: every state-changing POST on bunyip-web now refuses cross-origin form submissions before the handler runs. The audit's full spec is a per-session synchronizer-token middleware plus a hidden form input on every `<form>` (~25 templates), which is the right end-state but a real regression risk for the OIDC redirect chain. Ship the smaller fully-defensive step first: an `Origin` / `Referer` middleware that compares the submission's source host against the request's own `Host` header. What this DOES catch: - A cross-origin form on any other host POSTing to bunyip-web. Browsers send `Origin` reliably on cross-site POSTs; the mismatch trips the middleware before the auth handler runs. - An attacker-controlled child app under `*.{app_domain}` submitting to bunyip-web. CSP's form-action was widened by BUNYIP-249/271 to include those origins, so this is the layer that closes the surface that opening created. The child app's Origin is on the child host, not the BFF host. What this does NOT catch: - A same-origin XSS that synthesizes a POST. CSP is the layer that stops that one; this middleware can't tell legit JS from injected JS within the same origin. - A browser that strips both Origin and Referer for some misconfigured reason. The middleware fails closed (403); the user retries from a normal-config browser. The error body names the cause so users understand why a request was refused. Exemptions: the `/oauth2/*` family is exempt because the spec gates those POSTs on PKCE + state + nonce + client authentication. Cross-origin OIDC flows are exactly what the spec expects an OP to accept; refusing them here would break every legitimate RP integration. The Stripe webhook lives on bunyip-api, not bunyip-web, so it's not affected. Follow-up: a synchronizer-token middleware on top of this is its own ticket. It's defense in depth on top of the Origin check, not a substitute. The token threading needs every `<form>` template to render a hidden input and is heavier than this PR's scope. #BUNYIP-259
fix(web): Origin / Referer CSRF defense on every state-changing POST
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 55s
Check / fmt + clippy + build + tests (pull_request) Successful in 31m31s
Create release / Create release from merged PR (pull_request) Has been skipped
25fe79cf72
BUNYIP-259: every state-changing POST on bunyip-web now refuses cross-origin form submissions before the handler runs. The audit's full spec is a per-session synchronizer-token middleware plus a hidden form input on every `<form>` (~25 templates), which is the right end-state but a real regression risk for the OIDC redirect chain. Ship the smaller fully-defensive step first: an `Origin` / `Referer` middleware that compares the submission's source host against the request's own `Host` header.

What this DOES catch:

- A cross-origin form on any other host POSTing to bunyip-web. Browsers send `Origin` reliably on cross-site POSTs; the mismatch trips the middleware before the auth handler runs.
- An attacker-controlled child app under `*.{app_domain}` submitting to bunyip-web. CSP's form-action was widened by BUNYIP-249/271 to include those origins, so this is the layer that closes the surface that opening created. The child app's Origin is on the child host, not the BFF host.

What this does NOT catch:

- A same-origin XSS that synthesizes a POST. CSP is the layer that stops that one; this middleware can't tell legit JS from injected JS within the same origin.
- A browser that strips both Origin and Referer for some misconfigured reason. The middleware fails closed (403); the user retries from a normal-config browser. The error body names the cause so users understand why a request was refused.

Exemptions: the `/oauth2/*` family is exempt because the spec gates those POSTs on PKCE + state + nonce + client authentication. Cross-origin OIDC flows are exactly what the spec expects an OP to accept; refusing them here would break every legitimate RP integration. The Stripe webhook lives on bunyip-api, not bunyip-web, so it's not affected.

Follow-up: a synchronizer-token middleware on top of this is its own ticket. It's defense in depth on top of the Origin check, not a substitute. The token threading needs every `<form>` template to render a hidden input and is heavier than this PR's scope.

#BUNYIP-259
YousifShkara deleted branch fix/BUNYIP-259-csrf-synchronizer-token 2026-06-30 07:05:13 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!291
No description provided.