Skip to content

fix(auth0-express): harden createRouteUrl against path injection attacks - #23

Merged
frederikprijck merged 8 commits into
mainfrom
fix/route-url-path-validation
Jul 31, 2026
Merged

frederikprijck merged 8 commits into
mainfrom
fix/route-url-path-validation

Conversation

@cschetan77

@cschetan77 cschetan77 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Problem

`createRouteUrl` is a utility that combines a route path with the application base URL. When the path argument can be influenced by an attacker, insufficient validation allows the base URL origin to be overwritten — enabling open redirect and URL injection attacks.

What was fixed in a prior commit (7c69b9b)

Commit `7c69b9b` introduced two protections:

  1. Strip all leading forward slashes — changed `ensureNoLeadingSlash` from removing a single `/` to removing all of them, preventing protocol-relative URL attacks like `///evil.com`.
  2. Origin check after construction — after calling `new URL(path, base)`, the result's origin is compared to the base URL's origin and an error is thrown if they differ.

What remained unmitigated

1. Backslashes not stripped

`ensureNoLeadingSlash` only handled forward slashes. The WHATWG URL parser normalises backslashes to forward slashes, so inputs like `\\evil.com` or `\/evil.com` were treated as protocol-relative URLs (`//evil.com`) — overriding the base host.

2. No defence-in-depth before URL construction

Inputs like `javascript:alert(1)`, `data:text/html,...`, or `file:///etc/passwd` had no leading slashes so the multi-slash check was skipped entirely. The origin check caught these as a single safety net, but that was insufficient defence-in-depth.

What this PR fixes

`ensureNoLeadingSlash`

Strips both leading forward slashes and backslashes using `/^[/\\]+/`, closing the backslash normalisation bypass.

`AUTHORITY_URL_REGEX` — absolute URL detection

Detects inputs with an authority component (`://`) and routes them to a dedicated absolute-URL branch. This correctly handles absolute-form `req.url` values forwarded by reverse proxies. Inputs without `://` (including `javascript:`, `data:`, `report:summary`) are treated as relative paths.

`SCHEME_PREFIX_REGEX` — relative path colon guard

When a relative path's first segment contains a colon (e.g. `report:summary`, `news:today`), the WHATWG parser would misinterpret it as a scheme. Prepending `./` forces correct relative resolution. Legitimately dangerous schemes are blocked first by the denylist below.

`UNSAFE_SCHEME_REGEX` — denylist for unsafe schemes

Explicitly rejects `javascript:`, `data:`, and `vbscript:` before any URL construction. These are the only schemes that can execute code or render arbitrary content in a browser as a navigation target.

`createRouteUrl`

Validation now runs in three sequential gates:

  1. Authority URL branch — inputs with `://` are parsed and origin-checked directly.
  2. Multi-leading slash/backslash check — blocks `//evil.com`, `\\evil.com`, `/\evil.com`.
  3. Unsafe scheme denylist + `./` prefix — blocks `javascript:`/`data:`/`vbscript:`; safely resolves legitimate first-segment colon paths.

`toSafeRedirect`

Simplified to a try/catch wrapper around `createRouteUrl` — all validation lives in one place, eliminating duplicated regex logic and the previous double base-URL parse.

Attack vectors guarded against

Attack Example Blocked by
Protocol-relative via multiple `/` `//evil.com`, `///evil.com` Gate 2: multi-leading slash check
Backslash normalisation bypass `\\evil.com`, `\/evil.com`, `/\evil.com` Gate 2: multi-leading slash/backslash check
External absolute URL `https://evil.com/path\` Gate 1: origin check
Same host, different port `https://myapp.com:8443/\` Gate 1: origin check
`file://` traversal `file:///etc/passwd` Gate 1: `null` origin
Malformed absolute URL `https://` Gate 1: try/catch → descriptive Error
`javascript:` injection `javascript:alert(1)` Gate 3: unsafe scheme denylist
`data:` injection `data:text/html,...` Gate 3: unsafe scheme denylist
`vbscript:` injection `vbscript:msgbox(1)` Gate 3: unsafe scheme denylist
ASCII whitespace before scheme `\thttps://evil.com` Gate 3: final origin check
Legitimate first-segment colon paths `report:summary`, `news:today` Allowed — `./` prefix resolves as same-origin path

Testing

New and updated test cases (`packages/auth0-express/src/utils.spec.ts`):

  • `throws when path has multiple leading slashes/backslashes` — `\\evil.com`, `/\evil.com`, `\/evil.com`
  • `throws when path uses javascript/data/vbscript scheme` — blocked by denylist
  • `throws when path uses file:// scheme` — blocked by origin check
  • `accepts an absolute same-origin URL` — reverse proxy cases, with and without subpath base
  • `allows bare first-segment colon paths` — `report:summary`, `news:today`
  • `allows paths whose first segment starts with a digit followed by a colon` — `/2024-report:summary`
  • `single leading backslash stripped and resolves as same-origin path` — documents safe asymmetry
  • `toSafeRedirect` — backslash bypass attempts, subpath preservation, invalid base URL, unsafe schemes

@cschetan77
cschetan77 force-pushed the fix/route-url-path-validation branch from 3e822ff to f6e10b6 Compare July 13, 2026 05:22
Strip leading backslashes in addition to forward slashes to prevent
WHATWG URL parser normalisation bypasses, and add an explicit scheme
check before URL construction to reject javascript:, data:, file: and
other scheme-based injection attempts as defence-in-depth.

toSafeRedirect is updated to branch on whether input carries a scheme:
absolute URLs are validated via origin check directly, while relative
paths continue through the strict createRouteUrl validation.
…ardening

- Fix scheme regex from /^[a-zA-Z0-9.+-]*:/ to /^[a-zA-Z][a-zA-Z0-9.+-]*:/
  so paths with a digit-start first segment (e.g. /2024-report:summary) or
  a bare leading colon (:param) are not misidentified as URL schemes
- Extract SCHEME_REGEX to a named constant shared by createRouteUrl and
  toSafeRedirect to prevent validation drift
- Fix ensureNoLeadingSlash: change * to + and remove the dead g flag
- Move new URL(safeBaseUrl).origin inside the try block in toSafeRedirect
  so a malformed safeBaseUrl returns undefined instead of throwing uncaught
- Replace createRouteUrl(req.url, appBaseUrl) in callback-handler with
  new URL(req.url, appBaseUrl) + origin check, so absolute-form request
  targets forwarded by reverse proxies are handled correctly
- Add tests for single leading backslash, digit-start colon path, colon in
  non-first segment, toSafeRedirect backslash cases, and malformed safeBaseUrl
createRouteUrl now accepts absolute same-origin URLs (e.g. from a reverse
proxy) by branching on whether the input carries a scheme: absolute inputs
are parsed directly and validated by origin check, relative inputs go through
the existing slash-stripping and multi-leading check. This fixes the subpath
deployment regression where new URL('/auth/callback', base) was dropping the
subpath, causing redirect_uri mismatches and invalid_grant errors.

callback-handler.ts reverts to using createRouteUrl so subpath handling is
preserved and there is a single authoritative URL resolution helper.

toSafeRedirect is simplified to new URL(input, base) + origin check —
new URL() already handles both absolute and relative inputs natively, making
the previous scheme-branching logic redundant.
Simplifying toSafeRedirect to new URL(input, base) dropped the subpath
when resolving relative paths — new URL('/auth/callback', 'https://myapp.com/subapp')
ignores the subpath, producing https://myapp.com/auth/callback instead of
https://myapp.com/subapp/auth/callback. Restore the scheme-branching so
relative paths go through createRouteUrl which strips the leading slash
before resolving against the full base, preserving the subpath.
@cschetan77
cschetan77 force-pushed the fix/route-url-path-validation branch from 5d84d83 to d3cba15 Compare July 27, 2026 11:47
…ardening

- Replace SCHEME_REGEX with AUTHORITY_URL_REGEX (:// required) so bare
  first-segment-colon paths like `report:summary` and `javascript:alert(1)`
  are not misclassified as absolute URLs — they now resolve safely as
  relative paths via the `./` prefix trick
- Add SCHEME_PREFIX_REGEX to detect first-segment colons on the relative
  branch and prepend `./` so the WHATWG parser treats them as relative paths
- Wrap `new URL(url)` in a try/catch to surface a descriptive Error instead
  of a raw TypeError for malformed absolute inputs
- Simplify toSafeRedirect to delegate entirely to createRouteUrl, removing
  the duplicated AUTHORITY_URL_REGEX test and redundant origin re-check
- Add tests: bare first-segment colon paths, javascript:/data: as relative,
  toSafeRedirect subpath preservation, toSafeRedirect file:// rejection,
  toSafeRedirect javascript: safe resolution
…outeUrl

The ./prefix trick (introduced to fix bare first-segment colon paths like
report:summary) inadvertently allowed javascript:, data:, and vbscript:
inputs to resolve as same-origin paths. While harmless for res.redirect()
today, these schemes must never be valid route or redirect destinations.

Add UNSAFE_SCHEME_REGEX denylist check before the ./prefix step so these
three schemes are explicitly rejected. Legitimate first-segment colon paths
like report:summary and news:today are unaffected since they do not match
the denylist. Tests updated accordingly.
@frederikprijck
frederikprijck merged commit b6f9afb into main Jul 31, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants