Redirects are useful after sign-in, checkout, account setup, and many other workflows. The danger appears when an application lets request data decide the destination without enforcing where that destination may point.
For example, a sign-in page might accept a next value and redirect the user there after authentication. If any absolute URL is accepted, an attacker can create a link on the application’s real domain that sends the user to an unrelated site. The first URL looks legitimate, but the final destination is controlled by someone else.
This is an open redirect: a redirect endpoint that can be made to send users to destinations outside the application’s intended trust boundary. Open redirects do not automatically compromise an account, but they can make phishing more convincing, move users into an untrusted origin after a sensitive flow, or become a dangerous building block when another component assumes redirects stay on trusted sites.
This article develops a practical mental model for redirect safety, shows why simple string checks fail, and explains when relative paths, destination identifiers, or explicit allowlists are appropriate.
Treat the destination as a security decision
A redirect is not only navigation. It is a server decision about where the client should go next.
A useful mental model is:
request chooses intent
server chooses allowed destinationThat distinction changes the design. Instead of asking, “Is this user-supplied URL probably okay?”, ask, “Which destinations does this workflow actually need, and how can the server represent that set directly?”
The threat model here is a caller who can control a redirect-related value such as next, return_to, continue, or redirect. The control aims to stop that value from selecting an unintended external destination.
It does not protect a trusted destination that has itself been compromised. It also does not replace authentication, authorization, phishing-resistant authentication, or browser protections. The control is specifically about keeping navigation inside the destinations the application intends to trust.
Start with the smallest unsafe design
Consider a simplified sign-in flow:
next = request.query["next"]
if authentication succeeds:
redirect(next)The problem is not the redirect operation itself. The problem is that the request supplies the complete authority to choose the destination.
If the application only needs to return users to pages on its own site, accepting arbitrary URLs gives the input much more power than the feature requires.
A safer interface can accept a local path instead:
next = validated_local_path(request.query["next"])
if authentication succeeds:
redirect(next)The important work is hidden inside validated_local_path. It must establish that the final navigation stays within the intended origin or application route space. A production implementation should use the framework’s well-tested URL and redirect facilities rather than a hand-written parser when suitable facilities exist.
Even better, when the number of valid destinations is small, the request does not need to contain a path at all.
destination = request.query["destination"]
routes = {
"dashboard": "/dashboard",
"settings": "/settings"
}
redirect(routes.get(destination, "/"))Now the request chooses from server-defined meanings. It cannot introduce a new host, scheme, port, or path syntax because those details are not part of the input.
This is a recurring security principle: reduce untrusted input to the minimum authority the feature needs.
Why URL validation is harder than a prefix check
A common attempted defense is to accept a URL only when its text begins with a trusted-looking string.
Conceptually:
if target starts with "https://example.com":
redirect(target)This is fragile because URLs have structure. The scheme, host, port, path, query, and fragment have different meanings. A string that visually begins with expected characters is not necessarily interpreted as the intended origin.
The defensive lesson is not to memorize a collection of tricky URL spellings. It is to compare the security property you care about after parsing the URL according to the same rules the redirect mechanism uses.
If the policy is “HTTPS on example.com using the expected port”, then validate those parsed components. Do not substitute substring searches, suffix checks, or ad hoc regular expressions for URL parsing.
The same rule applies to local redirects. A value that looks path-like should be validated with semantics appropriate to the framework and deployment. Browsers and frameworks may interpret some forms of URL references differently from ordinary filesystem-style paths, so assumptions such as “it starts with a slash, therefore it is local” should not be treated as a portable security rule.
Prefer destination identifiers when the set is small
The simplest strong design is often to avoid accepting URLs.
Suppose an application has three valid post-login destinations:
home -> /home
billing -> /billing
projects -> /projectsThe client can submit the identifier projects, and the server can map it to /projects. This design has several advantages.
The allowed set is explicit. Code review can see every destination. A malformed identifier can fall back to a known route. URL parsing does not become part of the trust decision. If routes change, the server updates the mapping without changing the security model.
This approach is especially suitable for fixed workflow transitions such as onboarding steps, account settings, or a small set of application landing pages.
Its limitation is flexibility. An application with thousands of valid internal return locations may not want a table entry for every path. In that case, a validated local-path design can be reasonable, provided the framework’s redirect semantics are understood and tested.
Use an allowlist when external redirects are genuinely required
Some applications intentionally redirect to other origins. A central identity service may return users to several applications. A payment workflow may hand control to a small set of approved services. A multi-domain product may have legitimate cross-origin transitions.
In those cases, “never redirect externally” is not a useful rule. The server needs a precise allowlist.
The allowlist should describe the actual security boundary. Depending on the system, that may include:
allowed scheme
allowed host
allowed port
possibly an allowed path prefix or exact callback pathFor example, if only one HTTPS origin is valid, the policy should express that origin rather than merely checking whether the hostname contains a company name.
Avoid broad rules such as “any subdomain is trusted” unless every current and future subdomain is intended to receive the redirected user. Subdomains often have different owners, deployment processes, or security properties. A wildcard makes all of them part of the redirect trust boundary.
When a path restriction matters, apply it after the URL has been parsed and the origin has been accepted. Keep origin validation and path authorization conceptually separate so that a permissive path rule cannot accidentally widen the set of hosts.
Validate the final value, not an earlier representation
Security checks become unreliable when validation and use operate on different interpretations of the same input.
A risky pattern looks like this:
validate(raw_target)
normalized_target = transform(raw_target)
redirect(normalized_target)If transform can change the security-relevant meaning, the validation no longer proves that the value actually used is allowed.
A stronger sequence is:
parse and normalize according to defined rules
validate the security-relevant components
redirect the validated representationThe exact operations depend on the URL library and framework. The important invariant is that validation must apply to the same destination semantics that the redirect operation will use.
Be cautious about repeated decoding or multiple layers that each reinterpret a target. A proxy, framework, application router, and browser can all participate in navigation. If one layer validates one representation while a later layer changes it, the trust decision may be invalidated.
The practical response is to keep the redirect path simple: parse once with an appropriate URL facility, reject ambiguous or unsupported forms, construct the accepted destination from validated components where possible, and test the behavior through the deployed stack.
Do not rely on a confirmation page as the main control
Some systems respond to an external redirect with a page saying, “You are leaving this site.” That warning can be useful when external navigation is an intentional product feature, but it does not turn an unrestricted redirect into a well-bounded one.
Users may click through warnings mechanically. Automated clients may follow redirects without displaying the page. Other application components may care only that the trusted service issued a redirect, not whether a human saw a warning.
If the application knows the legitimate external destinations, enforce that set first. A warning can then provide additional context for users crossing an intentional trust boundary.
Keep sensitive data out of redirect targets
A redirect target can be exposed in browser history, application logs, proxy logs, analytics, referrer information under some policies and navigation patterns, screenshots, or copied links. That makes it a poor place for secrets.
Do not place passwords, long-lived credentials, API keys, or other reusable secrets in redirect URLs. Authentication protocols that carry temporary values through browser redirects need protocol-specific protections and should be implemented according to their specifications rather than by inventing a custom token-passing scheme.
This issue is related to open redirects but distinct from it. A perfectly allowlisted redirect can still leak sensitive URL data if the workflow puts secrets in the destination.
Verify the control with boundary-focused tests
A redirect defense is easier to trust when tests express the intended boundary directly.
For a local-only redirect, test that normal internal destinations succeed and that external absolute destinations are rejected or replaced with a safe fallback. Also test malformed, empty, and ambiguous values supported by the input surface.
For an external allowlist, test each allowed origin and nearby disallowed cases. The goal is not to build an endless catalogue of attack strings. The goal is to prove that changing a security-relevant component changes the decision as expected.
Useful properties include:
allowed host + allowed scheme -> accepted
unlisted host -> rejected
unexpected scheme -> rejected
unexpected port -> rejected when port is restricted
malformed target -> rejected
missing target -> safe defaultIntegration tests are valuable because redirect behavior can depend on framework helpers, reverse proxies, routing configuration, and URL normalization. A unit test of a string-checking function may miss a difference introduced elsewhere in the request path.
Logging rejected redirect targets can help diagnose integration mistakes and detect unusual use, but avoid logging secrets or unnecessary personal data. Logging is a detection and troubleshooting aid, not the enforcement mechanism.
Common failure modes come from granting too much choice
Most redirect problems are easier to understand as authority problems than as URL trivia.
Accepting a complete URL when the application needs only a page identifier grants unnecessary choice. Checking that a hostname contains a trusted word confuses text resemblance with origin identity. Trusting every subdomain expands the boundary beyond what many systems actually control. Validating one representation and redirecting another creates a gap between the security check and the operation.
Another common mistake is silently falling back to the unvalidated target when validation fails because preserving the user journey seems more convenient. Failure handling is part of the security design. If a target is invalid, redirect to a known safe location or return an error appropriate to the workflow. Do not turn validation failure into permission to bypass validation.
Choose the narrowest design that fits the workflow
There is no need for complex URL policy when a simpler representation solves the problem.
Use server-side destination identifiers when the set of valid destinations is small and known. Use carefully validated local destinations when users genuinely need to return to many routes within one application. Use an explicit origin allowlist when cross-origin redirects are required. Add path restrictions when only specific endpoints on an allowed origin should receive the flow.
The more flexible the redirect feature becomes, the more carefully its trust boundary must be defined and tested.
These controls reduce the risk that untrusted input can turn a trusted application into a launcher for arbitrary destinations. They do not make the destination trustworthy in every other sense. Authorization still belongs at the destination, sensitive operations still need their own protections, and external systems remain separate security boundaries.
Conclusion
A safe redirect design starts by deciding how much destination choice the request actually needs.
Do not begin with an arbitrary URL and try to remove every dangerous spelling. Represent a small set of destinations with server-side identifiers when possible. When URLs are necessary, parse them with appropriate URL semantics, validate the components that define your trust boundary, and redirect only the validated result.
The practical rule is simple: let untrusted input express where the user wants to go only within a destination set the server has already decided to trust.