ASVS 5.0 requirement V3.7.2 says the application may automatically redirect a user to a different hostname or domain that it does not control only when the destination appears on an allowlist. It is a Level 2 requirement.
This is the open redirect rule. Any ?next=, ?returnUrl= or ?redirect= parameter must be checked before you send the browser there.
Why it matters
An open redirect lets an attacker craft a link on your trusted domain that bounces the visitor to a hostile site: https://app.example.com/login?next=https://evil.example. Phishing emails with your real hostname in the link get through filters and fool people. It is also a building block for stealing OAuth codes and tokens when redirect checks are loose. See CWE-601.
Weak checks are a pattern of their own. Looking for example.com anywhere in the URL accepts https://example.com.evil.net and https://evil.net/?example.com. Checking that the value starts with / accepts //evil.net, which browsers treat as a protocol relative URL, and /\evil.net, which some browsers normalize into one.
How to implement it
Prefer local paths. Accept only a path and build the full URL yourself. When you need external destinations, parse the URL and compare the host to an exact allowlist.
Express:
const ALLOWED_HOSTS = new Set(["partner.example.org", "docs.example.com"]);
function safeRedirectTarget(raw, fallback = "/") {
try {
const url = new URL(raw, "https://app.example.com");
if (url.origin === "https://app.example.com") {
return url.pathname + url.search; // same site: path only
}
if (url.protocol === "https:" && ALLOWED_HOSTS.has(url.hostname)) {
return url.href;
}
} catch (e) { /* fall through */ }
return fallback;
}
app.get("/go", (req, res) => res.redirect(safeRedirectTarget(req.query.next)));
Django ships a helper:
from django.utils.http import url_has_allowed_host_and_scheme
def go(request):
target = request.GET.get("next", "/")
if not url_has_allowed_host_and_scheme(target, allowed_hosts={"app.example.com", "docs.example.com"}, require_https=True):
target = "/"
return redirect(target)
Alternatives that avoid parsing entirely: map short keys to destinations (?to=docs) so the user never supplies a URL, or sign the redirect target with an HMAC.
Also validate Location headers built from any user data, and the client side too: code that sets window.location from a parameter needs the same check.
Click through it at your own pace, or open it full screen.
How to test it
Probe every redirecting endpoint with a set of hostile values:
for v in "https://evil.example" "//evil.example" "/\\evil.example" \
"https://app.example.com.evil.example" "https://evil.example%2f@app.example.com" \
"javascript:alert(1)" "https:evil.example"; do
printf '%s -> ' "$v"
curl -s -o /dev/null -w '%{redirect_url}\n' -G https://app.example.com/go --data-urlencode "next=$v"
done
Every line must show your fallback or an allowlisted URL, never evil.example. Include login, logout, OAuth callback and language switch endpoints. A static code scan can find redirect calls fed by request parameters, as in the demo above.
Add these inputs as automated tests so a refactor cannot reintroduce the hole.
Common mistakes
Substring or prefix checks. Parse the URL and compare hostnames exactly.
Starts with a slash. Protocol relative forms get through.
Denylist of bad domains. You will never list them all.
Checking before decoding. Validate the final, decoded value.
Forgetting JavaScript schemes. Allow only https, and http only if truly needed.
Client side redirects. location.href = params.get("next") is the same bug.
Quick check
Is it safe to redirect to any URL after showing a warning page?
That is a separate control, see V3.7.3. The allowlist is the baseline, the interstitial is an additional layer for unavoidable external links.
Do I need an allowlist for my own subdomains?
Subdomains of your own domain are under your control, but list them explicitly so a forgotten or hijacked subdomain cannot be used.
Sources
- OWASP ASVS 5.0, requirement V3.7.2
- OWASP Unvalidated Redirects and Forwards Cheat Sheet
- CWE-601 on Gold