fix(security): reject consent scopes outside the original request

The /consent POST handler trusted the scope values submitted in the form,
so a forged consent submission could approve (and persist consent for)
scopes that were never part of the originating authorization request —
a scope-escalation vector.

Intersect the submitted scopes with the originally requested set stored in
the session before saving consent and completing the flow.

Refs: porchlight-a03

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Johan Lundberg 2026-06-04 10:26:55 +02:00
parent c52778326e
commit cdde3e3754
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
2 changed files with 28 additions and 2 deletions

View file

@ -338,8 +338,10 @@ async def consent_submit(request: Request) -> Response:
return RedirectResponse(f"{redirect_uri}?{params}", status_code=303)
return HTMLResponse("<h1>Error</h1><p>Invalid action</p>", status_code=400)
# Allow — collect approved scopes
approved_scopes: list[str] = [str(s) for s in form.getlist("scope")]
# Allow — collect approved scopes, rejecting anything outside the
# originally requested set (a forged form must not escalate scope).
requested_scopes = set(auth_params.get("scope", "openid").split())
approved_scopes: list[str] = [str(s) for s in form.getlist("scope") if str(s) in requested_scopes]
if "openid" not in approved_scopes:
approved_scopes = ["openid", *list(approved_scopes)]