GHSA-2WVM-8MVP-22QV
Vulnerability from github – Published: 2026-08-28 16:28 – Updated: 2026-08-28 16:28Summary
The 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.
Details
For 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.
File: frontend/src/routes/authorize/+page.ts (line 14) parses the raw redirect_uri 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 login_required must be returned), the page builds the callback URL from the raw redirect_uri and performs:
window.location.href = `${callbackURL}?error=login_required...`
The 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.
This 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.
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 redirect_uri 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 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).
- Attacker constructs an authorization URL pointing at the victim's pocket-id, supplying an external redirect_uri and prompt=none:
https://idp.victim.example/authorize?client_id=&redirect_uri=https://attacker.example/collect&response_type=code&scope=openid&prompt=none
- Victim opens the link. The frontend authorize page loads, determines the silent auth cannot succeed (login_required), and executes:
window.location.href = "https://attacker.example/collect?error=login_required&state=..."
- 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.
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/001_open_redirect_evidence.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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 2.8.0"
},
"package": {
"ecosystem": "Go",
"name": "github.com/pocket-id/pocket-id/backend"
},
"ranges": [
{
"events": [
{
"introduced": "2.6.0"
},
{
"fixed": "2.9.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-55834"
],
"database_specific": {
"cwe_ids": [
"CWE-601"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-28T16:28:20Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"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\u0027s 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\u0027s 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\u0027s 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\u0027s 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\u0027s pocket-id, supplying an external redirect_uri and prompt=none:\n\n https://idp.victim.example/authorize?client_id=\u003cknown_client_id\u003e\u0026redirect_uri=https://attacker.example/collect\u0026response_type=code\u0026scope=openid\u0026prompt=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\u0026state=...\"\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\u0027s 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.",
"id": "GHSA-2wvm-8mvp-22qv",
"modified": "2026-08-28T16:28:20Z",
"published": "2026-08-28T16:28:20Z",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Pocket-ID has an Open Redirect on the OIDC /authorize page via unvalidated redirect_uri with prompt=none"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.