Skip to content

fix(console): validate continue param in /auth/authorize to prevent open redirect (CWE-601)#26373

Closed
sebastiondev wants to merge 1 commit intoanomalyco:devfrom
sebastiondev:fix/cwe601-authorize-open-ca51
Closed

fix(console): validate continue param in /auth/authorize to prevent open redirect (CWE-601)#26373
sebastiondev wants to merge 1 commit intoanomalyco:devfrom
sebastiondev:fix/cwe601-authorize-open-ca51

Conversation

@sebastiondev
Copy link
Copy Markdown

@sebastiondev sebastiondev commented May 8, 2026

Issue for this PR

No public tracking issue — this PR fixes a security vulnerability (CWE-601, Open Redirect) and we did not want to publish exploit details in a public issue before a fix landed. Happy to open one retroactively, or to redirect this through a private security channel if maintainers prefer; please advise.

Type of change

  • Bug fix (security)
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

The console's /auth/authorize route concatenates the user-supplied continue query parameter directly into the OAuth callback URL without validation. After the OAuth round-trip, the callback handler derives a redirect target from the callback path, which means a crafted continue value can redirect the user to an attacker-controlled origin once authentication completes.

Affected file: packages/console/app/src/routes/auth/authorize.ts

Data flow:

  1. /auth/authorize?continue=<X> reads continue from the query string.
  2. It builds callbackUrl = new URL("./callback" + cont, request.url) and passes it to AuthClient.authorize(...) as the OAuth redirect_uri.
  3. After the IdP round-trip, [...callback] computes next = url.pathname.replace("/auth/callback","") and calls redirect(route(locale, next)).
  4. route() returns next verbatim for paths it doesn't specifically rewrite, so a next of //evil.com/x is passed through.
  5. Browsers resolve a Location: //evil.com/x response as https://evil.com/x — the user is redirected off-site.

A payload like /auth/authorize?continue=//evil.com/phish is sufficient. The attacker doesn't need any privileges; the victim only needs to click the link.

The fix adds a safeContinue() validator at the entry point that only accepts values which:

  • begin with a single / (relative path, not protocol-relative),
  • do not start with //, \, or /\ (protocol-relative / backslash variants that some browsers normalize),
  • contain no .., backslashes, or CR/LF/TAB characters.

Anything else is replaced with an empty string, which produces the safe default callback URL /auth/callback. The change is 18 lines, scoped to the single vulnerable file, and preserves the legitimate behavior of continue for in-app post-login navigation.

function safeContinue(value: string): string {
  if (!value) return ""
  if (!value.startsWith("/")) return ""
  if (value.startsWith("//") || value.startsWith("/\\") || value.startsWith("\\")) return ""
  if (value.includes("..") || value.includes("\\") || /[\r\n\t]/.test(value)) return ""
  return value
}

Why this works: the vulnerable primitive is "./callback" + cont resolved against request.url. By rejecting any cont that isn't a single-slash relative path with no traversal/backslash/control characters, the resolved callbackUrl can no longer escape the /auth/callback prefix on the legitimate origin, so the next derived later in the callback handler can no longer be turned into a protocol-relative or off-origin redirect.

How did you verify your code works?

Walked the validator against each attack class and the legitimate inputs by hand:

Input Result Notes
"" "" default callback
/workspace/abc /workspace/abc legitimate path preserved
//evil.com/x "" protocol-relative blocked
/\evil.com "" backslash variant blocked
\\evil.com "" backslash variant blocked
/foo/../../bar "" traversal blocked
https://evil.com "" absolute URL blocked (no leading /)
value containing CR/LF "" header injection blocked

The rest of the auth flow is untouched, so existing OAuth behavior for valid relative continue values is preserved. I did not add an automated unit test because the surrounding routes don't currently have unit-test coverage in this package; happy to add one if maintainers point me at the preferred test harness.

Adversarial review before submitting: checked whether SolidStart, route(), or the surrounding framework normalizes protocol-relative URLs before redirecting — they don't. Checked whether the OAuth IdP would always reject the manipulated redirect_uri — that depends on IdP configuration and isn't guaranteed (OpenAuth-style IdPs commonly accept path-prefix redirect_uri values). Checked whether there were existing access controls on /auth/authorize — there aren't; it's an unauthenticated GET.

Screenshots / recordings

N/A — no UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions github-actions Bot added needs:compliance This means the issue will auto-close after 2 hours. needs:issue labels May 8, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

This PR doesn't fully meet our contributing guidelines and PR template.

What needs to be fixed:

  • No issue referenced. Please add Closes #<number> linking to the relevant issue.

Please edit this PR description to address the above within 2 hours, or it will be automatically closed.

If you believe this was flagged incorrectly, please let a maintainer know.

@sebastiondev
Copy link
Copy Markdown
Author

Thanks — I've updated the PR description to follow the template (Type of change, What does this PR do, How did you verify, Screenshots, Checklist all filled in).

On the linked-issue point: this PR fixes a security vulnerability (CWE-601, Open Redirect) and I deliberately didn't open a public issue first to avoid publishing exploit details before a fix landed. I've explained that in the new "Issue for this PR" section. If maintainers would prefer this go through a private security channel (security advisory / SECURITY.md contact) instead, I'm happy to close this and resubmit there — please advise.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

This pull request has been automatically closed because it was not updated to meet our contributing guidelines within the 2-hour window.

Feel free to open a new pull request that follows our guidelines.

@github-actions github-actions Bot removed the needs:compliance This means the issue will auto-close after 2 hours. label May 8, 2026
@github-actions github-actions Bot closed this May 8, 2026
@sebastiondev
Copy link
Copy Markdown
Author

Re-submitted as #26507 with a high-level tracking issue (#26506) so the pr-standards bot is satisfied without publishing exploit details. Closing this one in favour of the new PR.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant