Skip to content

fix(consent): widen CSP form-action to the client redirect_uri#35

Merged
babs merged 1 commit into
masterfrom
fix/csp-form-action-redirect-uri
May 28, 2026
Merged

fix(consent): widen CSP form-action to the client redirect_uri#35
babs merged 1 commit into
masterfrom
fix/csp-form-action-redirect-uri

Conversation

@babs

@babs babs commented May 28, 2026

Copy link
Copy Markdown
Owner

What

Follow-up to #33 (which added the upstream IdP origin to the consent page's form-action). This adds the client's validated redirect_uri origin to form-action per render.

Why

When the user already has a live session with the upstream IdP, the consent POST's redirect chain doesn't terminate at an IdP login page — it stays in one navigation:

POST /consent → IdP authorize 302 → /callback 302 → client redirect_uri 302

Chromium enforces form-action across the entire chain, so without the client's redirect_uri origin in the directive Chrome blocks the final hop (surfacing a misleading "violates form-action" error naming /consent).

How

  • buildConsentCSP split into buildConsentCSPSources (static list computed once at startup: 'self', IdP origin, CSP_FORM_ACTION_EXTRA) + formatConsentCSP (per-render, appends the request's redirect_uri origin).
  • The redirect_uri origin is filtered through the same CSP3 §2.4 host-source check as operator extras (config.CanonicalCSPHostSource, now exported) — DCR validation accepts hosts with sub-delims (;, ,, …) that would break out of the directive, so a malicious DCR client cannot weaken its own consent page's CSP. On failure the page renders with the unwidened CSP and a consent_csp_redirect_uri_skipped warn (carrying client_id + redirect_uri) fires.
  • Per-render append uses slices.Concat (fresh allocation) rather than append into the shared startup slice — safe under concurrent renders.

Docs (configuration.md, threat-model.md) updated to match.

When the upstream IdP session is already live, the consent POST's
redirect chain stays in one navigation (POST /consent → IdP authorize
→ /callback → client redirect_uri) and Chromium enforces form-action
across every hop. Without the client's redirect_uri origin in
form-action, Chrome blocks the final hop. Append it per render,
filtered through the CSP3 host-source check so a DCR-registered
redirect_uri whose host smuggles a sub-delim cannot break out of the
directive.
@babs babs merged commit 942164a into master May 28, 2026
7 checks passed
@babs babs deleted the fix/csp-form-action-redirect-uri branch May 28, 2026 18:51
babs added a commit that referenced this pull request Jun 4, 2026
Chromium enforces the consent page's form-action directive against
every hop of the redirect chain a form submit initiates. Client-side
hops after redirect_uri (e.g. Power Platform's
global.consent.azure-apim.net -> regional UI origin) are unknowable
in advance, so origin enumeration (#33 IdP extras, #35 redirect_uri)
could never be complete.

- answer POST /consent (approve/deny/server-error) with a 200
  same-origin interstitial (meta refresh, no JS) instead of a 302;
  form navigation ends at the proxy, downstream hops are a regular
  navigation that form-action does not govern
- tighten consent page CSP to form-action 'self'; drop
  buildConsentCSPSources/formatConsentCSP and per-render widening
- deprecate CSP_FORM_ACTION_EXTRA (parsed, ignored, startup warn)
- re-render consent page with fresh JTI on replayed submit instead
  of a dead-end 400 consent_replay; decision still requires a new
  explicit click, single-use guarantee unchanged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant