Open redirect: only ever redirect to paths you control
Validate redirect targets as internal paths so your domain can't be used to bounce visitors to phishing
The threat
Your app has an endpoint that takes a URL or path in a query parameter and sends the visitor there with an HTTP redirect, something like /login?next=/dashboard or /out?url=https://.... The finding is that the destination is not checked: whatever you put in that parameter, the app will bounce the browser to, including a URL on a completely different domain.
That turns your own domain into a redirect service for attackers. They send a victim a link that genuinely starts with https://yourdomain.com/... (so it survives a glance, and any filter that trusts your domain), and your app quietly forwards them to a phishing page dressed up as your login. The linkβs trust is yours; the landing page is theirs. Good for their scam, bad for your name.
The exact fix
The rule: never redirect to a value taken straight from user input. Treat the parameter as an internal path, validate it, and fall back to a safe default.
Allowlist internal paths (the simplest safe pattern). Accept only same-site paths that start with a single /, and explicitly reject absolute URLs (https://...) and protocol-relative ones (//evil.example.com, which browsers treat as absolute):
function safeRedirect(target, res, fallback = '/dashboard') {
// must be a path, not a URL: starts with one slash, and the 2nd char is a normal path char
const ok = typeof target === 'string' && /^\/[^/\\]/.test(target);
res.redirect(ok ? target : fallback);
}
app.get('/login', (req, res) => {
// ...authenticate...
safeRedirect(req.query.next, res);
});
Why the
//and\\checks matter://evil.example.comand/\evil.example.comcarry no scheme, so they look like paths, but browsers resolve both to a different host. A naive βdoes it start with/?β check waves them straight through. Requiring the second character to be a normal path character closes that gap.
Validate the host against a known set if you truly need to redirect off-site (say, a few named partner domains). Parse the value against your own origin and only allow hosts you listed:
const allowedHosts = new Set(['yourdomain.com', 'app.yourdomain.com']);
function safeExternal(target, res, fallback = '/') {
try {
const u = new URL(target, 'https://yourdomain.com');
if (allowedHosts.has(u.host)) return res.redirect(u.href);
} catch { /* not a valid URL */ }
res.redirect(fallback);
}
Or map indirectly: never expose the URL at all. Pass a short key (?next=dashboard) and look the real path up in a server-side table, so nothing the user sends is ever used as a destination directly.
Verify it
curl -sI "https://yourdomain.com/login?next=https://evil.example.com" | grep -i location
The Location header must point back at your own domain (or your safe fallback like /dashboard), never at evil.example.com. Repeat with ?next=//evil.example.com and ?next=/\evil.example.com, both must also refuse to leave your site.
Proof
Because confirming an open redirect means actually following a crafted parameter off-domain, ApeCyber never does that unasked. It is surfaced only under the authorization-gated demonstration protocol, as a single non-destructive GET that checks whether a common redirect parameter returns a 3xx pointing off your domain, no tampering, no data touched. Once the endpoint only ever redirects to validated internal paths, that check comes back clean, a β20 off your posture score.