Skip to content

fix: sanitize returnPathname decoded from OAuth state (CWE-601)#24

Closed
nicknisi wants to merge 1 commit intomainfrom
fix/sanitize-return-pathname
Closed

fix: sanitize returnPathname decoded from OAuth state (CWE-601)#24
nicknisi wants to merge 1 commit intomainfrom
fix/sanitize-return-pathname

Conversation

@nicknisi
Copy link
Copy Markdown
Member

Summary

  • 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 (https://evil.com, //evil.com, /\evil.com, javascript:…) could flow into a downstream SDK's Location header and redirect the user off-origin after a successful sign-in — a credential-phishing primitive (CWE-601).
  • Centralizes 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 change

The returned returnPathname is now always a string beginning with exactly one /, safe to emit directly as a relative Location header. 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

  • authkit-sveltekit was actively vulnerable to this via its callback handler (fixed at the SDK layer in fix: prevent open redirect in OAuth callback (CWE-601) authkit-sveltekit#15).
  • authkit-tanstack-start is not vulnerable to open-redirect (its buildRedirectUrl happens to anchor to the trusted origin) but its redirect target still passes through this value, so it benefits from the tighter contract.
  • authkit-nextjs already reconstructs at the SDK layer, but no reason for every SDK author to re-derive the same mitigation.

Fixing once here removes an entire class of bug from every current and future consumer.

`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.
@nicknisi nicknisi closed this Apr 16, 2026
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 16, 2026

Greptile Summary

This PR centralizes CWE-601 open-redirect protection at the library layer by adding sanitizeReturnPathname in src/utils.ts. The function passes the untrusted returnPathname (decoded from the OAuth state parameter) through the WHATWG URL parser against a throwaway origin, then reconstructs the result as pathname + search + hash with a leading-slash normalization — stripping any smuggled host, scheme, backslash, or whitespace before the value is returned from handleCallback.

Confidence Score: 5/5

Safe 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

Filename Overview
src/utils.ts Adds sanitizeReturnPathname using WHATWG URL parser to strip hostile schemes/hosts and enforce a single leading slash; implementation is correct and robust.
src/utils.spec.ts Comprehensive test suite covering all major attack vectors (protocol-relative, javascript:, data:, backslash, whitespace smuggling) plus legitimate paths and fallback edge cases.
src/service/AuthService.ts Correctly threads rawReturnPathname as unknown through sanitizeReturnPathname before returning; both legacy and new-format state parsing paths are covered.

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]
Loading

Reviews (1): Last reviewed commit: "fix: sanitize returnPathname decoded fro..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant