The developer learned his lesson and decided to accept only relative URLs, checking that the URL starts with a slash. But a URL starting with two slashes — a protocol-relative URL — passes the check and sends the browser to another site.
The same Ravens login page, but the developer blocks absolute URLs and requires next to start with a slash. The simple text check does not tell a true relative path from a protocol-relative URL.
Make the redirect land on the external site falcon.owl.lab despite the leading-slash check.
You made the redirect land on an external site despite the slash check.
Why the solution worked
A protocol-relative URL starts with two slashes then a host, so it looks relative to a naive text check, but the browser treats it as an absolute URL to another site. A text prefix check is not enough.
The root cause
The check relies on a text prefix instead of parsing the URL, and two slashes pass it yet yield an absolute URL. The root is not parsing the destination and verifying its host.
How the code is fixed
A pure relative path starts with one slash, not two and not a slash-then-backslash. Parse the URL instead of checking only the prefix.
Vulnerable code
app.post('/login', (req, res) => {
const next = req.body.next;
// "Relative only": must start with a slash. //host passes this.
if (!next.startsWith('/')) return res.status(400).end();
res.redirect(next);
});
Safe code
app.post('/login', (req, res) => {
const next = req.body.next;
// A pure relative path: one leading slash, and not a protocol-relative //host.
if (!/^/[^/\]/.test(next)) return res.redirect('/dashboard');
res.redirect(next);
});
Impact in the real world
Protocol-relative URLs have bypassed many redirect filters and built phishing links on trusted domains.
How defenders detect it
Defenders parse the URL and reject anything that is not a pure relative path (one leading slash, not two), comparing the resulting host.