Login flows often need to remember where a user was going. A request arrives for /billing, the application sends the user to sign in, then redirects them back after authentication. The feature is useful, but it becomes an open redirect when an untrusted value can make the application send the browser to an arbitrary destination.
That matters because the redirect begins on a domain the user already trusts. A crafted link can legitimately reach your application and then immediately send the browser somewhere you never intended. Redirect parameters can also cross security boundaries in authentication and authorization flows when code assumes that “after login” is automatically a trusted place.
The defensive rule is simple: treat every redirect destination influenced by the requester as untrusted input. Decide which destinations the application actually needs, validate against that policy, and fall back to a known location when the value doesn’t qualify. The details matter because URL syntax contains several forms that are easy to mistake for harmless paths.
A redirect destination is a routing decision
Consider a common login flow:
GET /login?next=/billing
successful login
|
v
redirect to /billingHere, next isn’t ordinary display data. The server uses it to decide where the browser goes next. That makes it part of a trust boundary: input from the request controls navigation performed under the application’s authority.
The smallest unsafe design is conceptually:
next = request.query["next"]
authenticate_user()
redirect(next)If next can name any URL, the application has delegated its redirect decision to the requester. The problem isn’t the HTTP redirect mechanism itself. Redirecting to a fixed application route is normal. The problem is allowing untrusted input to select a destination outside the set the application intends to support.
A better mental model is:
untrusted destination
|
v
parse according to URL rules
|
v
allowed by redirect policy? -- no --> known safe destination
|
yes
v
redirectThe validation step should answer a policy question, not merely a syntax question. A perfectly valid URL can still be an unacceptable redirect destination.
Define the threat model before writing the validator
Redirect validation mainly reduces the risk that your application can be used as a trusted-looking path to an unintended site or security boundary. It also helps keep post-authentication navigation inside the locations your flow was designed to handle.
It does not prove that an allowed destination is trustworthy in every other sense. If an allowed page contains a separate vulnerability, redirect validation doesn’t fix it. The control also doesn’t stop phishing links that point directly to another domain, compromised DNS, malicious content hosted on a domain you deliberately allow, or authorization mistakes at the destination.
The key design question is narrower: which destinations should this particular redirect feature be able to select?
For many login flows, the answer is “paths within this application.” That is much easier to defend than “any URL that looks reasonable.” Other features, such as an outbound link service, may genuinely need external destinations. Those require a different policy and should not inherit a same-origin validator by accident.
Prefer identifiers when the destination set is small
The strongest simplification is to avoid accepting a URL at all.
Suppose a workflow has only three valid post-action destinations: account settings, billing, and the dashboard. Instead of accepting an arbitrary next URL, accept a small identifier and map it on the server:
"account" -> /account
"billing" -> /billing
"dashboard" -> /dashboard
anything else -> /dashboardNow the requester can choose only among destinations the application already defined. There is no URL parser edge case to turn an external address into an accepted value.
This approach works well for bounded workflows such as onboarding steps, administrative actions, or redirects after a small set of forms. It becomes awkward when the application genuinely needs to return users to many internal pages. In that case, accepting an internal path can be reasonable, but the path still needs a precise policy.
For same-origin redirects, validate parsed URL structure
A common requirement is: “after login, return to another page on this application.” Developers sometimes implement that as “allow values beginning with /.” That test is too weak.
URI syntax distinguishes a path beginning with one slash from a reference beginning with two slashes. A value such as:
/profileis an absolute-path reference. In contrast, a value beginning with // is a network-path reference: it contains an authority component and can identify another host when resolved by a URL-aware client.
That is why redirect policy should be based on parsed URL components or a framework helper with documented same-origin semantics, rather than a string prefix that merely looks path-like.
For a feature that accepts only local application paths, the policy can be expressed conceptually as:
parse candidate as a URL reference
accept only if:
scheme is absent
authority/host is absent
path is an allowed application path form
otherwise:
use a known local fallbackWhether you also allow query strings and fragments depends on the feature. If they are allowed, treat them as part of the destination and make sure downstream code handles them as untrusted request data. A local redirect doesn’t make its query parameters trusted.
Use your framework’s well-tested local-URL or safe-redirect helper when it provides one with semantics that match this policy. Avoid reimplementing URL parsing with regular expressions or ad hoc substring checks.
Don’t validate external redirects with substring tests
Some applications legitimately redirect to a small set of external origins: perhaps a separate account portal or a payment provider. In that case, compare the parsed destination against an explicit policy.
A check such as this is not a meaningful origin policy:
if "example.com" appears in destination:
allow redirectThe text can appear in places other than the hostname, and hostnames that merely contain the string are not automatically part of the intended trust boundary.
Instead, parse the URL and compare the components you actually mean to trust. A policy might require HTTPS and an exact host:
scheme == "https"
host == "accounts.example.com"If several hosts are valid, enumerate them or define a carefully bounded rule that matches the application’s ownership model. Be especially cautious with broad subdomain rules. Allowing every subdomain is only appropriate if every matching host is controlled to the level required by the redirect’s security purpose. A forgotten, delegated, user-controlled, or separately administered subdomain can invalidate that assumption.
Ports matter too. If the feature intends to send users to the normal HTTPS service, decide whether a non-default port belongs in the allowed origin rather than silently accepting any port on the same hostname.
Canonicalize by parsing, not by repeatedly decoding strings
URL validation becomes fragile when application code and the component consuming the redirect interpret the same text differently. One layer might check an encoded string while another decodes it, resolves a relative reference, or normalizes its structure before navigation.
The defensive goal is to make the validation decision on the same semantic URL components that will be used for the redirect. Parse once with a standards-aware URL implementation, reject malformed or policy-violating values, and pass the validated representation through the redirect mechanism in the form your framework expects.
Avoid building a custom pipeline that repeatedly decodes until a value “looks normal.” Multiple transformations create ambiguity about which representation was actually approved. They can also turn validation into a collection of special cases rather than a clear destination policy.
This is one reason framework redirect helpers are useful when their contract is understood: parsing and URL resolution have more edge cases than a prefix check suggests. The application should still define the policy; the library should handle the syntax.
Keep authorization separate from redirect validation
A safe redirect target is not necessarily an authorized resource.
Suppose /admin/reports is a valid local path. A normal user may still lack permission to view it. The redirect validator can correctly conclude that the destination stays within the application, but the reports endpoint must independently enforce authorization when the browser requests it.
The sequence should be:
login succeeds
-> validate return destination
-> redirect to approved local destination
-> destination handles new request
-> destination enforces its own authorizationDo not treat a validated next parameter as proof that the user is entitled to the resource. Redirect validation controls navigation; authorization controls access. Combining those concepts creates brittle flows where reaching an endpoint through an unexpected route may bypass an assumption made elsewhere.
The same separation applies before authentication. If a user originally requested a protected page, storing that path for later navigation is convenient, but the application must check authorization again after login because the authenticated identity is now known and permissions may have changed.
Decide what happens when validation fails
A validator needs a boring failure path. When a return target is missing, malformed, or outside policy, redirect to a known location such as the application dashboard or account home.
Do not “repair” an untrusted external URL into something that happens to pass. Rejecting the candidate is easier to reason about and easier to test.
For user experience, a fallback is usually better than returning a server error after a successful login. The user completed authentication; losing the optional return location shouldn’t normally invalidate that success. High-value workflows may choose a different response, but the behavior should be deliberate.
Security logging can record that a redirect candidate was rejected, especially when repeated failures are useful for detection. Avoid copying sensitive query values into logs merely to preserve the rejected URL. Record the event and the minimum fields needed for investigation.
Test the policy as a security boundary
Tests should describe the redirect contract in terms of accepted and rejected classes, not only one happy-path example.
For a same-origin return path, useful cases include:
- an ordinary local path that should be accepted;
- a local path with an allowed query string, if the feature supports one;
- an absolute URL that should be rejected;
- a network-path reference beginning with
//that should be rejected; - malformed input that should fall back safely;
- an empty or missing value that should use the default destination.
If external origins are allowed, add tests for exact approved origins and near-misses: wrong scheme, wrong host, unexpected port, and subdomains that the policy does not explicitly trust.
Test the actual redirect helper or endpoint rather than only a standalone validator when practical. Integration can change semantics. For example, a framework may normalize a value before placing it in the Location header, or a reverse proxy may affect how application code constructs absolute URLs. The final behavior is what the user agent receives.
Common mistakes come from validating the wrong property
Open redirect defenses often fail because the check proves something weaker than the application needs.
“Starts with /” proves only something about the first character, not that the reference has no authority component. “Contains our domain” proves a substring exists, not that the parsed host equals an approved host. “Uses HTTPS” protects transport to whichever host was selected; it does not make that host trusted. “The destination is URL-encoded” changes representation, not authority.
Another mistake is applying one global redirect rule to unrelated features. A post-login return path may need only local navigation, while an explicit “continue to partner” feature may need a small external allowlist. Give each redirect point the narrowest policy that supports its actual job.
Finally, don’t rely on the destination page to notice that the redirect was inappropriate. By the time an external site receives the browser, your application has already made the routing decision. Validate before issuing the redirect.
Choose the narrowest redirect capability the feature needs
Redirect validation is easier when you frame it as capability design rather than URL cleanup. A parameter that can choose among three server-defined destinations has less authority than one that can choose any local path. A local-path parameter has less authority than one that can choose arbitrary external origins.
Start with the narrowest form that satisfies the product requirement:
small fixed set? -> server-side identifier mapping
many internal pages? -> validated local URL/path
known external origins? -> parsed explicit allowlist
arbitrary external URL? -> reconsider the flow or make the external transition explicitThen verify the control at the real redirect boundary and keep destination authorization independent. The practical takeaway is not “sanitize URLs.” It is to decide where the application is willing to send a browser, encode that decision as a small explicit policy, and reject everything outside it.