Spot the vulnerability in this login redirect handler and explain how to fix it.
Vulnerable code
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await authenticate(username, password);
if (!user) return res.sendStatus(401);
req.session.userId = user.id;
const next = req.query.next ?? '/';
res.redirect(next); // ← dangerous: attacker controls next
});Fixed
app.post('/login', async (req, res) => {
const { username, password } = req.body;
const user = await authenticate(username, password);
if (!user) return res.sendStatus(401);
req.session.userId = user.id;
const raw = req.query.next ?? '/';
const next = isSafeRedirect(raw, req.headers.origin) ? raw : '/';
res.redirect(next);
});
function isSafeRedirect(url, origin) {
try {
const parsed = new URL(url, origin);
return parsed.origin === origin;
} catch {
return false;
}
}The handler redirects to the next query parameter without any validation — a classic open redirect. An attacker can craft a login link like https://yoursite.com/login?next=https://evil.com/phish and trick users into logging in and then landing on the attacker's page (which looks identical to a legitimate site). This is especially dangerous combined with OAuth: the next URL may appear after a legitimate authentication flow, lending it credibility.
The fix validates that next is a relative path on the same origin: new URL(next, req.headers.origin).origin === req.headers.origin. Using pathname alone is insufficient because //evil.com/path is a protocol-relative URL that resolves to evil.com — always parse with new URL() and compare the full origin.