{"id":"CVE-2026-86205","aliases":["GHSA-fp4x-ggrf-wmc6"],"url":"https://o3.security/vulnerability/CVE-2026-86205","summary":"h3 before 2.0.1-rc.18 Open Redirect via redirectBack()","details":"## Summary\n\nThe `redirectBack()` utility in h3 validates that the `Referer` header shares the same origin as the request before using its pathname as the redirect `Location`. However, the pathname is not sanitized for protocol-relative paths (starting with `//`). An attacker can craft a same-origin URL with a double-slash path segment that passes the origin check but produces a `Location` header interpreted by browsers as a protocol-relative redirect to an external domain.\n\n## Details\n\nThe vulnerable code is in `src/utils/response.ts:89-97`:\n\n```typescript\nexport function redirectBack(\n  event: H3Event,\n  opts: { fallback?: string; status?: number; allowQuery?: boolean } = {},\n): HTTPResponse {\n  const referer = event.req.headers.get(\"referer\");\n  let location = opts.fallback ?? \"/\";\n  if (referer && URL.canParse(referer)) {\n    const refererURL = new URL(referer);\n    if (refererURL.origin === event.url.origin) {\n      // BUG: pathname can be \"//evil.com/path\" which browsers interpret\n      // as a protocol-relative URL\n      location = refererURL.pathname + (opts.allowQuery ? refererURL.search : \"\");\n    }\n  }\n  return redirect(location, opts.status);\n}\n```\n\nThe root cause is a discrepancy between how the WHATWG URL parser and browsers handle double-slash paths:\n\n1. `new URL(\"http://target.com//evil.com/path\").origin` → `\"http://target.com\"` — origin check **passes**\n2. `new URL(\"http://target.com//evil.com/path\").pathname` → `\"//evil.com/path\"` — extracted as redirect location\n3. Browser receives `Location: //evil.com/path` → interprets as protocol-relative URL → **redirects to `evil.com`**\n\n**Attack scenario:** The attacker shares a link like `http://target.com//evil.com/page`. If the target application has catch-all routes (common in SPAs built with h3/Nitro), the app serves its page at that URL. When the user navigates to an endpoint calling `redirectBack()`, the browser sends `Referer: http://target.com//evil.com/page`. The origin check passes, and the user is redirected to `evil.com`, which can host a phishing page mimicking the target.\n\n## PoC\n\n```bash\n# 1. Create a minimal h3 app with redirectBack\ncat > /tmp/h3-redirect-poc.ts << 'SCRIPT'\nimport { H3, redirectBack } from \"h3\";\n\nconst app = new H3();\napp.post(\"/submit\", (event) => redirectBack(event));\n\nconst res = await app.fetch(new Request(\"http://localhost/submit\", {\n  method: \"POST\",\n  headers: { referer: \"http://localhost//evil.com/steal\" }\n}));\n\nconsole.log(\"Status:\", res.status);\nconsole.log(\"Location:\", res.headers.get(\"location\"));\n// Expected: a same-origin path\n// Actual: \"//evil.com/steal\" — protocol-relative redirect to evil.com\nSCRIPT\n\n# 2. Verify URL parsing behavior\nnode -e \"\nconst u = new URL('http://localhost//evil.com/steal');\nconsole.log('origin:', u.origin);         // http://localhost\nconsole.log('pathname:', u.pathname);     // //evil.com/steal\nconsole.log('origin matches localhost:', u.origin === 'http://localhost');  // true\n\"\n# Output:\n# origin: http://localhost\n# pathname: //evil.com/steal\n# origin matches localhost: true\n```\n\n## Impact\n\nAn attacker can redirect users from a trusted application to an attacker-controlled domain. This enables:\n\n- **Credential phishing**: Redirect to a lookalike login page to harvest credentials\n- **OAuth token theft**: In OAuth flows using `redirectBack()`, steal authorization codes by redirecting to an attacker's callback\n- **Trust exploitation**: Users see the initial link points to the trusted domain, lowering suspicion\n\nThe vulnerability requires no authentication and affects any endpoint using `redirectBack()`.\n\n## Recommended Fix\n\nSanitize the extracted pathname to prevent protocol-relative URLs. In `src/utils/response.ts`, after extracting the pathname from the referer:\n\n```typescript\nexport function redirectBack(\n  event: H3Event,\n  opts: { fallback?: string; status?: number; allowQuery?: boolean } = {},\n): HTTPResponse {\n  const referer = event.req.headers.get(\"referer\");\n  let location = opts.fallback ?? \"/\";\n  if (referer && URL.canParse(referer)) {\n    const refererURL = new URL(referer);\n    if (refererURL.origin === event.url.origin) {\n      let pathname = refererURL.pathname;\n      // Prevent protocol-relative open redirect (e.g., \"//evil.com\")\n      if (pathname.startsWith(\"//\")) {\n        pathname = \"/\" + pathname.replace(/^\\/+/, \"\");\n      }\n      location = pathname + (opts.allowQuery ? refererURL.search : \"\");\n    }\n  }\n  return redirect(location, opts.status);\n}\n```","published":"2026-09-06T12:00:25.530Z","modified":"2026-10-28T02:30:29.280312274Z","cvss":null,"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"npm","name":"h3","fixedVersion":"2.0.1-rc.18"}],"fix":{"url":"https://github.com/h3js/h3/commit/459a1c6593365b0810e9c502df7c3e82837321d7","label":"h3js/h3@459a1c6"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/86xxx/CVE-2026-86205.json"},{"type":"ADVISORY","url":"https://github.com/h3js/h3/security/advisories/GHSA-fp4x-ggrf-wmc6"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-86205"},{"type":"ADVISORY","url":"https://www.vulncheck.com/advisories/h3-before-2.0.1-rc.18-open-redirect-via-redirectback"},{"type":"WEB","url":"https://github.com/h3js/h3/commit/459a1c6593365b0810e9c502df7c3e82837321d7"},{"type":"PACKAGE","url":"https://github.com/h3js/h3"},{"type":"WEB","url":"https://github.com/h3js/h3/releases/tag/v2.0.1-rc.18"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:30:29.280312274Z"}}