SecPrep logoSecPrep

Spot the vulnerability in this login redirect handler and explain how to fix it.

Vulnerable code

javascript
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
});
Open Redirect

Fixed

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

Practice this in the app →