Standard login flow with a `?next=` parameter so users land back where they were before the login. One line. Mesrai's catch.
The vulnerable diff
// auth/login.ts
authRouter.post("/login", async (req, res) => {
const ok = await authenticate(req.body.email, req.body.password);
if (!ok) return res.status(401).json({ error: "bad credentials" });
// BUG: req.query.next is unvalidated
res.redirect((req.query.next as string) || "/");
});What is wrong
`res.redirect` happily honors any URL. Three forms of `next` are dangerous: an absolute URL (`https://evil.com/`), a protocol-relative URL (`//evil.com`), and a backslash-prefixed URL that some browsers normalize (`\\evil.com`). All three bounce the user out of your origin. This is CWE-601, Open Redirect.
Open redirect is often dismissed as low severity because it does not directly compromise the server. The real damage is reputational: attackers use your domain in phishing links because the URL looks legitimate up until the redirect fires. It is also a building block in chained attacks — combine with stolen-cookie scenarios and OAuth flows and the severity climbs fast.
The attack
# Phishing email contains:
https://app.example.com/login?next=//evil.example/fake-login
# User sees app.example.com in the URL bar, trusts it, signs in.
# After successful login the server runs:
res.redirect("//evil.example/fake-login")
# Browser interprets //evil.example as protocol-relative URL
# (inherits https). Lands on attacker page, often a clone of
# the post-login dashboard prompting "session expired, sign in".Mesrai's review comment
mesraipilot · Bot · reviewed 30 sec ago
[mesrai] [code-review] [Security] [CWE-601] [medium]
`res.redirect(req.query.next)` is an open redirect. Three forms of
`next` send the user off-origin:
next=https://evil.com (absolute URL)
next=//evil.com (protocol-relative)
next=\\evil.com (browser-normalized in some versions)
Validate that `next` is a same-origin path. The minimum check:
function safeNext(v: unknown): string {
if (typeof v !== "string") return "/";
// must start with "/" but not "//" or "/\"
if (!v.startsWith("/")) return "/";
if (v.startsWith("//") || v.startsWith("/\\")) return "/";
return v;
}
res.redirect(safeNext(req.query.next));
If you want to support cross-origin redirects to specific partner
domains, use a server-side allowlist rather than client input.
Reference: OWASP Unvalidated Redirects Cheat Sheet, CWE-601The fix
// auth/login.ts — fixed
function safeNext(v: unknown): string {
if (typeof v !== "string") return "/";
if (!v.startsWith("/")) return "/";
if (v.startsWith("//") || v.startsWith("/\\")) return "/";
// optional: cap length and strip control chars
if (v.length > 512) return "/";
return v;
}
authRouter.post("/login", async (req, res) => {
const ok = await authenticate(req.body.email, req.body.password);
if (!ok) return res.status(401).json({ error: "bad credentials" });
res.redirect(safeNext(req.query.next));
});The checks read as a one-liner each. `next` must start with `/`. It must not be `//` (protocol-relative). It must not be `/\\` (backslash-normalized). Length-capped to avoid silly payloads. If you genuinely need cross-origin redirects, use a server-side allowlist that the client cannot influence.
Why human review missed it
Open redirect is one of those bugs that does not look like a bug. The line says `redirect to whatever the client requested`, which sounds like correct behavior. The damage is downstream, in the chain of trust between your domain and the user's expectation. Senior reviewers catch it when they have seen the phishing scenario before; otherwise the line reads as feature, not vulnerability.
Related rules + further reading
Mesrai rule pack: security/no-open-redirect — flags `res.redirect` calls with unvalidated request input.
OWASP Cheat Sheet: Unvalidated Redirects and Forwards.
CWE-601 — URL Redirection to Untrusted Site.
Takeaway
Three checks on the `next` parameter close the open-redirect class for the entire app. Mesrai catches the pattern wherever `res.redirect` meets `req.query` or `req.body`.