Return users to this site only

After sign-in, the login page redirects to whatever its next parameter says, so a link to your own domain can end on a look-alike site. Return users only to a page on this site.

Requires Node.js 22 or newer. No package installation or server needed. To work in a container instead, open the folder with VS Code Dev Containers or GitHub Codespaces: .devcontainer/devcontainer.json uses the Dev Containers Node.js 24 image, pinned by digest.
1. Unzip all files into one folder.
2. In that folder, run: node --test exercise.test.mjs
3. Edit login-redirect.mjs, then run the same command again.

The starter deliberately fails some tests. A successful repair passes every case, including cases that catch common wrong fixes. The browser runs this same cases.mjs with the same fixtures; locally Node runs your code directly. If you completed the optional first stage in the browser, Download my work adds your input as stage1-input.json and the tests include it. Only run code you wrote or reviewed.

1. Reproduce the redirect
Run the starter. It returns next unchanged, so //evil.example/ and https://evil.example/ send the signed-in user to another site, and a repeated next parameter comes back as an array.

2. Accept only a path
Return "/account" unless next is a string that starts with one slash and not two. Then refuse any backslash and any control character: browsers read /\evil.example/ as //evil.example/, and the URL parser deletes a tab, so /<tab>/evil.example/ becomes //evil.example/ too.

3. Parse it the way the browser will
Parse the path with new URL(next, "https://app.example.test") and compare url.origin with the site’s origin. Return url.href, the parser’s own serialization: rebuilding the path from url.pathname turns /.//evil.example/ into //evil.example/.

4. Keep ordinary returns working
Paths with a query and a fragment, percent-encoded characters, a colon, or a URL inside the query string must return users exactly where they were. Do not decode the value again: /%2F%2Fevil.example/ is a harmless path until code decodes it twice.

SUPPLIED CONTRACT
redirectAfterLogin(next) returns the value of the Location header the sign-in response sends. next is the ?next= query parameter of the sign-in page as the framework decoded it: usually a string such as "/account/billing", undefined when it is absent, and an array when the parameter is repeated. The site is https://app.example.test and the default page is /account. Rules: accept next only when it is a path on this site, meaning a string that starts with exactly one slash and contains no backslash and no control character (U+0000 to U+001F and U+007F), and that still has the origin https://app.example.test after parsing with the URL class against that origin; return the parsed URL’s href, never a path rebuilt from its parts and never a decoded copy. Anything else, including absolute URLs, returns "/account". The tests resolve the returned value against https://app.example.test/login, as a browser resolves a Location header.

REFERENCE
reference.mjs contains an explained solution: /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.

SCOPE
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.

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?

SOURCE
https://url.spec.whatwg.org/#concept-basic-url-parser
