Open Redirect Prevention: A Developer's Guide to Safe Redirects
Three fixes stop most open redirect attacks: accept only relative paths for internal redirects, map short server-side tokens to destinations instead of trusting client-supplied URLs, and require an exact host allow-list plus an interstitial warning page for any external target. Open redirect prevention isn’t a single control. It’s a layered decision tree that starts with “do you even need this redirect to be dynamic?”
Most vulnerable code you’ll find in production skips that question and jumps straight to redirect(request.params.url). That single line is why open redirect vulnerabilities remain a fixture on bug bounty reports years after the pattern was first cataloged.
Here’s the priority order for a patch or sprint:
- Prefer relative paths. If your redirect only ever needs to send someone back to
/dashboardor/checkout, reject anything that isn’t a path starting with a single/. - Use server-side indirection. Map a short token or ID to a destination URL in your own database, so the client never controls the actual target string.
- Allow-list exact hosts, then confirm. When you genuinely need to send users off-domain, match the full host against a fixed list and show an outbound confirmation page before the jump.
The concrete code, the encoded-bypass tricks that break naive checks, and the framework helpers for ASP.NET Core, Express, and Django/Flask are all below.
Key Takeaways
Open redirect prevention works because it replaces trust in client-supplied URLs with server-side validation: relative paths, exact host allow-lists, and parsed comparisons instead of string matching.
| Point | Details |
|---|---|
| Default to relative paths | Reject absolute URLs for internal redirects; there’s rarely a real need for one. |
| Never string-match hosts | Parse the URL and compare .hostname exactly; startsWith() checks miss userinfo and protocol-relative bypasses. |
| Use server-side indirection | Map short tokens to destination URLs server-side so clients never control the actual target string. |
Require exact redirect_uri matching for OAuth |
Prefix or wildcard matching on OAuth redirect URIs is a documented token-theft vector. |
| Test at three layers | Combine SAST taint rules, DAST fuzzing of redirect parameters, and CI unit tests for redirect sinks. |
Table of Contents
- What Is Open Redirect Prevention and Why Does It Matter?
- What Are the Common Open Redirect Variants Attackers Use?
- How Do You Fix a Vulnerable Redirect in Code?
- What’s the Best Way to Prevent Open Redirects?
- How Do ASP.NET Core, Express, and Django/Flask Handle This?
- How Do You Test for Open Redirect Vulnerabilities?
- How Should Link Shorteners Handle Redirect Safety?
- Where Can You Read More on Open Redirect Standards?
- What Do Developers Consistently Get Wrong About This?
- Sources
What Is Open Redirect Prevention and Why Does It Matter?
Open redirect prevention means validating any URL your application redirects to, rather than trusting a client-supplied destination. The underlying flaw is cataloged as CWE-601: URL Redirection to Untrusted Site, and it happens whenever an app builds a redirect target from unvalidated input. OWASP classifies it as a distinct attack pattern precisely because the fix is so often skipped: a redirect looks harmless because the vulnerable server never serves malicious content itself.
That’s what makes it dangerous. Attackers don’t need your server to host anything bad. They need your domain’s reputation to get past an email security gateway or a suspicious user’s second glance, then hand off to wherever they actually want the victim to land.
Three consequences show up repeatedly in real incidents:
- Phishing evasion. A link that starts with your trusted domain sails past spam filters and cautious users, then silently redirects to a credential-harvesting page.
- OAuth and token theft. If an authorization flow’s
redirect_uriisn’t validated with exact matching, an attacker can redirect the authorization code or access token to a server they control. SANS/ISC calls open redirects “exploit building blocks” specifically because of this OAuth chaining risk. - SSRF and chain attacks. A redirect endpoint can become the first hop in a chain that reaches internal services never meant to be internet-facing.
Treat every redirect parameter as tainted input from the moment it enters your code, the same way you’d treat a SQL query parameter.
What Are the Common Open Redirect Variants Attackers Use?
Recognizing the pattern in your own codebase starts with knowing what it looks like in the wild.
- Query-parameter redirects. The classic case:
?next=,?returnUrl=,?redirect=, or?url=parameters that feed directly into a redirect call. These are the first thing an attacker fuzzes on any login or logout flow. - Header-based redirects. Some apps read a custom header or the
Referervalue and redirect based on it, which is just as exploitable as a query string, but easier to miss in a code review since it’s not sitting in the URL bar. - JavaScript-based redirects. Client-side code that does
window.location = getParam('next')has the identical flaw, just moved to the browser. It’s frequently missed because security reviews focus on server routes. - Meta-refresh and JS redirects embedded in generated pages. Template-injected redirect URLs in a
<meta http-equiv="refresh">tag behave the same way and are easy to overlook in template audits.
Attackers don’t stop at plain URLs. They obfuscate to slide past naive checks:
- Protocol-relative URLs like
//evil.example.comskip the scheme and still redirect the browser off-domain. - Embedded credentials such as
https://yourapp.com@evil.example.comtrick a check that only looks foryourapp.comas a substring. - Backslash tricks and mixed encoding exploit inconsistent parsing between browsers and your validation logic.
- Double-encoding (
%2568decoding to%68thenh) can defeat filters that only decode once.
These get chained into real attacks: a phishing email that routes through your login page’s next parameter, an OAuth flow where redirect_uri isn’t matched exactly, or an internal redirect service repurposed for SSRF against metadata endpoints.
How Do You Fix a Vulnerable Redirect in Code?
Here’s a minimal vulnerable pattern you’ve probably shipped at some point, in an Express-style handler:
app.get('/login', (req, res) => {
const next = req.query.next;
// ... authentication logic ...
res.redirect(next); // vulnerable: next is fully attacker-controlled
});
Anyone can set next=https://evil.example.com and your app happily forwards them there after a legitimate login, which is exactly the setup phishing campaigns rely on. The fix enforces that the target is a relative path on your own site:
app.get('/login', (req, res) => {
const next = req.query.next;
const safePath = (next && next.startsWith('/') && !next.startsWith('//'))
? next
: '/dashboard';
res.redirect(safePath);
});
Notice the second check: !next.startsWith('//'). That’s not decorative. //evil.example.com starts with a single slash character followed by another slash, and a naive startsWith('/') check alone lets it through, because browsers treat a leading double slash as protocol-relative and resolve it against whatever scheme the page is loaded on.
The same failure mode shows up with startsWith(trustedDomain) checks:
// vulnerable: substring/prefix match
if (target.startsWith('https://yourapp.com')) {
res.redirect(target);
}
An attacker sends https://yourapp.com@evil.example.com/path. The string starts with https://yourapp.com, so the check passes, but the browser parses yourapp.com as userinfo (a login username) and sends the user to evil.example.com. The robust fix parses the URL properly and checks the actual host, not a substring:
const { URL } = require('url');
function isSafeRedirect(target, allowedHosts) {
try {
const parsed = new URL(target, 'https://yourapp.com');
return allowedHosts.includes(parsed.hostname) &&
parsed.protocol === 'https:';
} catch {
return false;
}
}
Parsing with a standard library, as RFC 3986 defines the URL structure, eliminates the entire class of userinfo, backslash, and protocol-relative tricks in one move, because you’re comparing parsed.hostname against an exact list, not scanning a raw string for a pattern.
Pro Tip: Log every rejected redirect attempt, including the original raw value and the resolved host. That log becomes your earliest signal of both attempted phishing campaigns using your domain and misconfigured internal links breaking legitimate flows.
What’s the Best Way to Prevent Open Redirects?
Work through this checklist in order. Each step closes a gap the previous one leaves open.
- Eliminate the dynamic redirect if you can. The safest redirect is one that isn’t user-controlled at all. Many “return to previous page” features can use session state instead of a URL parameter.
- Prefer relative paths for anything internal. Reject absolute URLs outright for same-site navigation; there’s rarely a legitimate reason a post-login redirect needs a full URL with scheme and host.
- Use server-side indirection for anything more complex. Store a short token that maps to a destination URL server-side, as OWASP’s cheat sheet recommends, so the client passes an opaque ID and never controls the actual URL string.
- Allow-list exact hosts for legitimate external redirects. Compare the parsed hostname against a fixed list, never a prefix or substring match, and never a wildcard unless you fully control every subdomain on that list.
- Parse and normalize every input with a standard library. Never write your own URL parser; use your language’s built-in
URLclass or equivalent and read.hostnameand.protocolfrom the parsed result. - For OAuth flows, require exact
redirect_urimatching. SANS/ISC is specific on this: prefix matching or wildcard subdomains onredirect_uriis how token theft chains get built.
Denylists fail for a structural reason: they enumerate what you already know about, and attackers only need one variant you didn’t think of. A denylist that blocks javascript: URIs, for instance, still misses JaVaScRiPt: on a case-insensitive parser, or a URL-encoded variant your denylist regex never decodes before matching. CWE-601 recommends an “accept known good” strategy for exactly this reason. Allow-listing what you trust is finite and auditable; blocking what you distrust is an open-ended arms race you will lose eventually.
When an external redirect genuinely can’t be avoided, an interstitial page closes the gap between “technically validated” and “actually safe” for the human clicking the link. Show the destination URL in full, require a click to continue, and never auto-refresh past it in under a few seconds. This is standard guidance from CWE-601 and the OWASP cheat sheet alike, and it costs you one extra page load in exchange for giving a wary user a chance to notice they’re about to leave your domain.
How Do ASP.NET Core, Express, and Django/Flask Handle This?
Framework helpers exist specifically because hand-rolled validation is where most bugs creep in.
ASP.NET Core ships LocalRedirect() and IUrlHelper.IsLocalUrl() precisely for this problem. Microsoft’s guidance shows checking returnUrl with Url.IsLocalUrl(returnUrl) before calling LocalRedirect(returnUrl), and the helper explicitly rejects protocol-relative URLs and absolute URLs with a different host. If you’re calling Redirect() directly with a query-string value in an ASP.NET Core app, that’s the pattern to replace.
Node/Express has no built-in equivalent, so the discipline has to be manual: parse with the URL class, compare .hostname against your allow-list, and never use startsWith() or includes() against a raw string. For anything beyond a same-site path, route through a server-side mapping table rather than trusting a client-passed absolute URL.
Django provides django.utils.http.url_has_allowed_host_and_scheme(), which checks both host and scheme against an allow-list you pass in; it’s what powers Django’s own login-redirect protection. Flask doesn’t ship an equivalent, so the common pattern is checking that urlparse(target).netloc is empty (meaning it’s a relative path) before redirecting, or maintaining your own allow-list check against request.host.
If your framework doesn’t have a helper, or you’re on an older version where one was added later, fall back to the same three moves every time: parse with the standard library’s URL parser, exact-match the host against an allow-list, and default to a known-safe internal path whenever validation fails or throws. Never default to redirecting on parse failure.
How Do You Test for Open Redirect Vulnerabilities?
Detection needs to happen at three layers, or regressions slip back in every time someone refactors the redirect handler.
Static analysis (SAST). Configure taint-tracking rules that flag any redirect sink, such as res.redirect(), a raw Location header write, or a RedirectView, whenever the value traces back to unsanitized request input. Tools like Semgrep and CodeQL both support custom rules for this exact sink pattern, and pairing that with the parameter names cataloged by the WebAppSec open redirect research gives you a solid starting rule set.
Dynamic testing (DAST). Fuzz every known redirect parameter, next, returnUrl, redirect, url, continue, with payloads including https://evil.example, //evil.example, and userinfo variants like yourapp.com@evil.example. Assert on the actual Location header or final landing host, not just a 3xx status code.
CI enforcement. Write unit tests asserting that redirect functions reject off-host targets and return your safe default. Fail the build on any new SAST finding that touches a redirect sink. Log every rejected external redirect attempt in production, both for incident response and to catch legitimate use cases your allow-list missed.
How Should Link Shorteners Handle Redirect Safety?
Any service whose entire product is redirecting users somewhere else inherits this problem at scale, and gets it wrong just as often as individual apps do. A link shortener that lets anyone create a short link pointing anywhere, with no validation at creation time, is an open redirect generator by design.
The mitigations are the same ones covered above, applied at the platform level: validate and normalize the destination URL when a link is created, not just when it’s clicked; block known-bad target hosts outright; and show an interstitial for destinations that don’t match a trusted pattern. Analytics can still work without any of this compromising user privacy, since destination validation and non-identifying click telemetry (referrer, device, country) are separate concerns entirely.
Rdyrct builds redirect safety into link creation itself, validating destinations up front and pairing that with privacy-friendly analytics that never store IP addresses. Teams evaluating a managed link platform that treats redirect integrity as core infrastructure, not an afterthought, can see the full feature set at rdyrct.
Where Can You Read More on Open Redirect Standards?
- OWASP’s open redirect overview for the core attack definition and mitigation summary.
- OWASP’s Unvalidated Redirects and Forwards Cheat Sheet for detailed prevention patterns.
- CWE-601 for the formal weakness classification and mitigation strategies.
- Microsoft’s ASP.NET Core guidance for framework-specific helper methods.
- RFC 3986 for the authoritative URI structure reference.
Teams evaluating a redirect platform built with these safeguards in mind can review Rdyrct’s link management features directly.
What Do Developers Consistently Get Wrong About This?
Most teams treat open redirect prevention as a checkbox: add a startsWith() check, close the ticket, move on. That’s the conventional advice, and it’s exactly why the same bypasses, protocol-relative URLs, userinfo tricks, keep reappearing in bug bounty reports years after they were first documented. String matching feels like validation. It isn’t. It’s pattern matching against a pattern you already know, which is precisely what an attacker exploits.
The gap I keep seeing is between “we validate redirects” and “we parse redirects.” Those are different engineering decisions. Parsing with a standard URL library and comparing an exact hostname closes an entire bug class in one function; a denylist of bad substrings closes one bypass at a time, forever playing catch-up.
If you take one thing from this guide, take the priority order seriously: eliminate the dynamic redirect first, use server-side indirection second, and only reach for an allow-list plus interstitial when you genuinely need to send someone off-domain. Most teams skip straight to allow-listing and never ask whether the redirect needed to be dynamic at all.
rdyrct runs on Cloudflare Workers and is open source, with a free plan.
Sources
- Open Redirect | OWASP Foundation
- Prevent open redirect attacks in ASP.NET Core | Microsoft Learn
- CWE-601: URL Redirection to Untrusted Site (‘Open Redirect’)
- Open Redirect
- Unvalidated Redirects and Forwards - OWASP Cheat Sheet Series