Hint 1: Ask where the browser will go, not what the string starts with
A browser parses the Location value with the WHATWG URL rules: it deletes tabs and newlines, reads a backslash as a slash in http and https URLs, and treats //host as another site. Check the characters, then let new URL do the parsing.
Hint 1 unlocks after your first failed run.
Hint 2: Send exactly what you checked
Return the href of the URL object you validated. A decoded copy, or a path assembled from url.pathname, is a different value that your check never saw.
Hint 2 unlocks after 2 failed runs.
Reveal the reference patch and explanation
const SITE = 'https://app.example.test';
const DEFAULT = '/account';
export function redirectAfterLogin(next) {
// A path on this site: one leading slash, no backslash, no control character.
if (
typeof next !== 'string' ||
!next.startsWith('/') ||
next.startsWith('//') ||
next.includes('\\') ||
/[\u0000-\u001f\u007f]/.test(next)
)
return DEFAULT;
// Parse it as the browser will, and check that it stays on this origin.
const url = new URL(next, SITE);
if (url.origin !== SITE) return DEFAULT;
// Redirect to the parsed URL, not to a path rebuilt from its parts.
return url.href;
}/account/billing, /reports?month=2026-09#totals, /search?q=caf%C3%A9, /calendar/2026-10-11T09:30 and /import?source=https://feeds.example.org/rss come back as https://app.example.test plus the same path, query and fragment, and a missing or empty next gives /account. https://evil.example/, //evil.example/, ///evil.example/, /\evil.example/, \\evil.example/, /<tab>/evil.example/, https://app.example.test@evil.example/ (a user name before @), https://app.example.test.evil.example/, http:evil.example/, http://app.example.test/account and javascript:alert(document.domain) all fall back to /account, as does a repeated next. /.//evil.example/ stays on the site as https://app.example.test//evil.example/ because the parsed href is returned, and /%2F%2Fevil.example/ stays a harmless path because it is not decoded again.
Compare it with your own fix, then rerun the tests. The editor is not changed when you open this explanation.
The reference patch unlocks after you run code you have edited.
Inspect the actual test harness
The browser and the lab download run the same cases, including cases that catch common wrong fixes, such as comparing with one fixture value instead of the real rule. The download includes the complete test adapter and cryptographic fixtures.
Download cases.mjsHow the browser sandbox runs your code
Your code runs in QuickJS 2025-09-13 (quickjs-emscripten 0.32.0) inside a Web Worker, isolated from the page, with a 32 MiB memory limit and a 2.5-second time limit. The test cases and the pass or fail verdict run outside that sandbox, so changing built-ins such as Array.prototype.push cannot change the result. The lab download runs the same cases under Node.js, where your code runs directly.
The same as in current Node.js:
- ES2025 syntax and built-ins, including Set methods such as intersection, iterator helpers, Promise.try, RegExp.escape and the RegExp v flag
Added to the sandbox so that it behaves like Node.js:
- structuredClone, Array.fromAsync, queueMicrotask, atob and btoa
- URL and URLSearchParams, parsed by your browser’s own URL implementation
- TextEncoder and TextDecoder for UTF-8
- Intl.Collator and a locale-aware String.prototype.localeCompare
- console.log and related methods, whose output appears with your results
Not available in the browser sandbox:
- Timers (setTimeout, setInterval), fetch and other network or storage APIs; the exercises are deterministic and offline
- Node.js modules and globals: import, require, process and Buffer
- Web Crypto (crypto.getRandomValues, crypto.subtle); each workshop supplies the cryptographic operations it needs
- Intl other than Collator: toLocaleString and toLocaleDateString ignore the locale, so numbers have no digit grouping and dates use one fixed format in the device’s time zone
- using declarations, Symbol.dispose and DisposableStack (explicit resource management)
- Uint8Array.fromBase64 and toBase64, JSON.rawJSON, Atomics and Error.captureStackTrace
This is one redirect sink with a local-path rule. Other sinks need the same check: sign-out and language switchers, OAuth and SAML return parameters, Referer-based back links and client-side code that assigns location.href, where a javascript: URL also runs script. Destinations on other sites need an ID that maps to a fixed URL, or an exact origin allowlist; a local path that is itself a redirect endpoint, such as /out?url=, reopens the redirect. The site’s origin comes from configuration here, never from the Host header.
Your next task: List every place your application sends a browser to a URL that came from a request, a cookie or a stored field. Which of them parse the value the way a browser does before deciding?
WHATWG URL Standard: the basic URL parser
Related guide: Redirect users only to destinations you control