Skip to content

fix(auth): harden post-login redirect validation#125

Merged
rubenhensen merged 2 commits into
mainfrom
fix/validate-login-redirect-123
Jul 2, 2026
Merged

fix(auth): harden post-login redirect validation#125
rubenhensen merged 2 commits into
mainfrom
fix/validate-login-redirect-123

Conversation

@dobby-coder

@dobby-coder dobby-coder Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

The post-login redirect query param was passed straight into SvelteKit's redirect() (server load, +page.server.ts) and goto() (client, +page.svelte) without any validation. An attacker could craft a link such as:

  • ?redirect=https://evil.example
  • ?redirect=//evil.example (protocol-relative)

to bounce a freshly-logged-in user off-site — a classic open redirect (see draft advisory GHSA-mr47-cc7w-r794, tracked in #123).

Changes

  • Add a small, co-located, unit-tested helper src/routes/auth/login/safe-redirect.ts exporting safeRedirect(). It only accepts same-origin, path-absolute targets:
    • must start with a single /
    • rejects absolute URLs (https://…), protocol-relative //host, and the /\host backslash variant that browsers normalise to protocol-relative
    • falls back to /portal/dashboard for anything else (including empty/missing)
  • Wire the guard into +page.server.ts (redirect()) and into handleSuccess() in +page.svelte (before goto()).
  • Add tests/unit/safe-redirect.test.ts covering accepted paths, missing values, non-slash values, protocol-relative URLs, and the backslash bypass.

Testing

  • npx vitest run — 99 passed (14 files), including the 5 new cases.
  • npm run check (svelte-check) — 0 errors, 0 warnings.
  • prettier --check + eslint --max-warnings 0 on the changed files — clean.

Refs #123

The post-login `redirect` query param was passed straight to SvelteKit's
`redirect()` (server load) and `goto()` (client) without validation,
allowing an attacker to craft `?redirect=https://evil.example` or the
protocol-relative `?redirect=//evil.example` to bounce a logged-in user
off-site (open redirect).

Add a shared `safeRedirect()` helper that only accepts same-origin,
path-absolute targets (must start with a single `/`), rejecting absolute
URLs, protocol-relative `//host` and the `/\host` backslash variant that
browsers normalise to it, falling back to `/portal/dashboard`. Wire it into
both the server load and `handleSuccess()`, with unit tests.

Refs #123

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@dobby-coder dobby-coder Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rules Dobby 2 — consolidated review (cycle 1)

Verdict is request-changes — posted as COMMENT only because GitHub blocks self-request-changes on a bot-authored PR. Treat the blocking item below as blocking.

Ran the per-rule compliance checks (incl. repo-specific security notes) and merged them with Review Dobby 2's findings. One blocking security issue remains; the fix is otherwise well-structured and well-tested.

🔴 Blocking: open-redirect control-character bypass still exploitable

safeRedirect() still lets control characters through. safeRedirect('/\t/evil.example') (also /\n/… and /\r/…) starts with a single / and its 2nd char is neither / nor \, so it is accepted and returned unchanged. Browsers strip ASCII TAB/LF/CR from URLs (WHATWG URL spec), so /<tab>/evil.example normalises to //evil.examplehttps://evil.example/.

I reproduced this on the checked-out branch:

"/\t/evil.example" -> returned "/\t/evil.example" | new URL(out, origin): https://evil.example/
"/\n/evil.example" -> returned "/\n/evil.example" | new URL(out, origin): https://evil.example/
"/\r/evil.example" -> returned "/\r/evil.example" | new URL(out, origin): https://evil.example/

This affects both the server redirect(303, …) path (TAB is a valid Location header char) and the client goto() path — it is the exact class of bypass this PR exists to prevent.

Fix: reject control chars before the slash checks, e.g.

if (/[\u0000-\u001f]/.test(target)) return DEFAULT_REDIRECT;

and add a regression test covering /\t/, /\n/, /\r/.

🟡 Non-blocking: PR title names the vulnerability class

Per the no-public-security-issues rule, don't name the vuln in a public PR title while the advisory (GHSA-mr47-cc7w-r794) is still a draft. Consider a generic title such as fix(auth): harden post-login redirect validation — still conventional-commit compliant.

✅ Passing checks

  • Conventional-commit PR title format — OK
  • Tests present (rejection + happy paths) — OK
  • SvelteKit route exports (logic extracted to sibling safe-redirect.ts) — OK
  • Delivered scope matches the /dobby fix request — OK
  • No repo-specific security pitfalls introduced — OK

Sending back to the PR Dobby to fix the control-char bypass.

Comment thread src/routes/auth/login/safe-redirect.ts Outdated
Browsers strip C0 control characters (TAB/LF/CR) from URLs before
navigating, so `/\t/evil.example` and `/\n//evil.example` slipped past
the slash checks and then normalised to a protocol-relative, off-origin
URL. Reject any C0 control char (U+0000-U+001F) up front, before the
slash checks. Add regression tests for the TAB/LF/CR variants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dobby-coder dobby-coder Bot changed the title fix(auth): validate post-login redirect to prevent open redirect fix(auth): harden post-login redirect validation Jul 2, 2026
@rubenhensen rubenhensen marked this pull request as ready for review July 2, 2026 07:16
@rubenhensen rubenhensen merged commit 259c5ff into main Jul 2, 2026
17 checks passed
@rubenhensen rubenhensen deleted the fix/validate-login-redirect-123 branch July 2, 2026 07:17
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.

1 participant