{"id":"CVE-2026-55834","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-55834","summary":"Pocket-ID has an Open Redirect on the OIDC /authorize page via unvalidated redirect_uri with prompt=none","details":"### Summary\nThe OIDC authorization page in the pocket-id frontend redirects the browser to an attacker-controlled URL without consulting the backend redirect_uri allow-list when the request uses prompt=none. An attacker who knows a valid client_id 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.\n\n### Details\nFor an OIDC authorization request with prompt=none, the SvelteKit frontend short-circuits the flow before the backend ever validates the redirect_uri against the client's registered callback list.\n\nFile: frontend/src/routes/authorize/+page.ts (line 14) parses the raw redirect_uri query parameter from the incoming URL.\n\nFile: 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 login_required must be returned), the page builds the callback URL from the raw redirect_uri and performs:\n\n    window.location.href = `${callbackURL}?error=login_required...`\n\nThe callbackURL is taken directly from the user-supplied redirect_uri. 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 redirect_uri matches one of the client's registered callback URLs) is never invoked on this branch.\n\nThis means the per-client redirect_uri allow-list, the central control that makes redirect_uri safe in OAuth/OIDC, is not enforced for the prompt=none error-return path. A client_id is not a secret: client metadata is retrievable at /api/oidc/clients/:id/meta, and client_ids appear in any integration's authorization links.\n\nConfirmation 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.\n\nA 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.\n\nContrast: the standard interactive flow (no prompt=none) posts to the backend, which validates redirect_uri against the registered list before returning a callback. The defect is specific to the client-side prompt=none short-circuit.\n\n### PoC\nPrerequisites: a running pocket-id instance, a registered OIDC client whose client_id 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 login_required branch).\n\n1. Attacker constructs an authorization URL pointing at the victim's pocket-id, supplying an external redirect_uri and prompt=none:\n\n   https://idp.victim.example/authorize?client_id=<known_client_id>&redirect_uri=https://attacker.example/collect&response_type=code&scope=openid&prompt=none\n\n2. Victim opens the link. The frontend authorize page loads, determines the silent auth cannot succeed (login_required), and executes:\n\n   window.location.href = \"https://attacker.example/collect?error=login_required&state=...\"\n\n3. The browser is redirected to https://attacker.example/collect. Expected result on a fixed build: the page would refuse the redirect because redirect_uri does not match the client's registered callback list and would render an error in place instead of navigating off-origin.\n\nValidation 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/001_open_redirect_evidence.txt.\n\n### Impact\nUnauthenticated 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.","published":"2026-08-28T16:28:20Z","modified":"2026-08-28T16:45:06.911780167Z","cvss":{"score":4.3,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Go","name":"github.com/pocket-id/pocket-id/backend","fixedVersion":"2.9.0"}],"fix":{"url":"https://github.com/pocket-id/pocket-id/commit/8a7577497131229badb35cb4b3a4227b1300afff","label":"pocket-id/pocket-id@8a75774"},"references":[{"type":"WEB","url":"https://github.com/pocket-id/pocket-id/security/advisories/GHSA-2wvm-8mvp-22qv"},{"type":"WEB","url":"https://github.com/pocket-id/pocket-id/commit/8a7577497131229badb35cb4b3a4227b1300afff"},{"type":"PACKAGE","url":"https://github.com/pocket-id/pocket-id"},{"type":"WEB","url":"https://github.com/pocket-id/pocket-id/releases/tag/v2.9.0"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-28T16:45:06.911780167Z"}}