Now the site accepts a URL only if it contains the trusted domain as a string. But the userinfo part of a URL (before an @) lets you place the trusted domain where it does not set the real host, so the actual host becomes the attacker site.
The same Ravens login page, but the check now accepts a URL if it merely contains the trusted domain name as text. A substring check confuses the different parts of a URL.
Make the redirect land on falcon.owl.lab even though the URL contains the trusted domain.
You made the redirect land on the attacker site despite the substring allowlist.
Why the solution worked
The real host is what comes after the @, not before it, so placing the trusted domain in the userinfo part passes the contains-check while the browser goes to the actual host, the attacker site. A text check does not understand URL structure.
The root cause
The allowlist compares text instead of parsing the URL and extracting the host, and the userinfo part fools the comparison. The root is trusting a substring, not the parsed host.
How the code is fixed
Compare the parsed host by exact match against a domain list; never search for a substring inside the raw URL.
Vulnerable code
const ALLOWED = 'ravens.lab';
app.post('/login', (req, res) => {
// Substring allowlist: the trusted name can hide in the userinfo part.
if (!req.body.next.includes(ALLOWED)) return res.status(400).end();
res.redirect(req.body.next);
});
Safe code
const ALLOWED = new Set(['login.ravens.lab', 'www.ravens.lab']);
app.post('/login', (req, res) => {
const url = new URL(req.body.next, 'https://login.ravens.lab');
// Compare the PARSED host by exact match, not a substring of the raw URL.
res.redirect(ALLOWED.has(url.host) ? url.href : '/dashboard');
});
Impact in the real world
Host confusion with @ and fake subdomains has bypassed substring allowlists and built convincing phishing links on major domains.
How defenders detect it
Defenders parse the URL and compare the resulting host against an allowlist of domains by exact match, never a substring.