fix: sanitize returnPathname decoded from OAuth state (CWE-601)#24
fix: sanitize returnPathname decoded from OAuth state (CWE-601)#24
Conversation
`handleCallback` decoded `returnPathname` from the OAuth `state` parameter and returned it to callers verbatim. Because `state` round-trips through the IdP and is attacker-influenceable, a crafted value such as `https://evil.com`, `//evil.com`, or `/\evil.com` could flow into a downstream SDK's `Location` header and redirect a user off-origin after a successful sign-in — an open-redirect / phishing primitive. Centralize the fix at the library layer so every SDK inherits a safe value. A new `sanitizeReturnPathname` utility parses the untrusted value against a throwaway origin (the WHATWG URL parser strips any smuggled host, scheme, backslash, tab, or newline) and rebuilds a same-origin relative path as `pathname + search + hash` with a leading-slash normalization that defuses the `//evil.com` protocol- relative case. The `fallback` parameter goes through the same transform so a caller's custom fallback can't reopen the hole. Contract: the returned `returnPathname` is always a string beginning with exactly one `/`, safe to emit as a relative `Location` header. Adds 21 tests covering hostile inputs, legitimate path/query/hash preservation, and fallback-parameter safety.
Greptile SummaryThis PR centralizes CWE-601 open-redirect protection at the library layer by adding Confidence Score: 5/5Safe to merge — the sanitization logic is correct, the test coverage is comprehensive, and no custom rules are violated. All three files are clean. The WHATWG URL-parser approach correctly strips hostile hosts and schemes across every tested attack vector (protocol-relative, javascript:, data:, backslash, whitespace injection). The fallback path is also sanitized, and the final backstop to '/' is correct. No P0 or P1 findings. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[OAuth Callback: code + state] --> B[authenticateWithCode]
B --> C[Save encrypted session]
C --> D{state contains '.'}
D -- Yes: new format --> E[base64-decode internal segment]
D -- No: legacy format --> F[base64-decode full state]
E --> G[rawReturnPathname = parsed.returnPathname]
F --> H{has returnPathname?}
H -- Yes --> I[rawReturnPathname = parsed.returnPathname]
H -- No --> J[customState = state]
G --> K
I --> K
K[sanitizeReturnPathname rawReturnPathname]
K --> L{typeof input === 'string' and non-empty?}
L -- No --> M[toSafePath fallback]
L -- Yes --> N[new URL input, 'https://placeholder.invalid']
N --> O["'/' + pathname.replace leading slashes + search + hash"]
O --> P[Safe origin-relative path]
M --> Q{fallback parseable?}
Q -- Yes --> P
Q -- No --> R["return '/'"]
P --> S[Return: response, headers, returnPathname safe, state, authResponse]
Reviews (1): Last reviewed commit: "fix: sanitize returnPathname decoded fro..." | Re-trigger Greptile |
Summary
handleCallbackdecodedreturnPathnamefrom the OAuthstateparameter and returned it to callers verbatim. Becausestateround-trips through the IdP and is attacker-influenceable, a crafted value (https://evil.com,//evil.com,/\evil.com,javascript:…) could flow into a downstream SDK'sLocationheader and redirect the user off-origin after a successful sign-in — a credential-phishing primitive (CWE-601).sanitizeReturnPathnameutility parses the untrusted value against a throwaway origin (the WHATWG URL parser strips any smuggled host, scheme, backslash, tab, or newline) and rebuilds a same-origin relative path aspathname + search + hashwith a leading-slash normalization that defuses the//evil.comprotocol-relative case.fallbackparameter goes through the same transform so a caller's custom fallback can't reopen the hole.Contract change
The returned
returnPathnameis now always a string beginning with exactly one/, safe to emit directly as a relativeLocationheader. Downstream SDKs that previously blindly echoed this value (e.g. authkit-sveltekit, see workos/authkit-sveltekit#15) are automatically patched after a version bump. Downstream SDKs that already sanitized (authkit-nextjs, authkit-tanstack-start) are unaffected.Motivation
buildRedirectUrlhappens to anchor to the trusted origin) but its redirect target still passes through this value, so it benefits from the tighter contract.Fixing once here removes an entire class of bug from every current and future consumer.