A login page often needs to remember where a user was going. After authentication, the application might read a next or return_to parameter and send the browser there. The feature looks harmless because the redirect happens only after the application has finished its real work.
The problem appears when that parameter can name any destination. An attacker can then distribute a link on your trusted domain that immediately sends visitors somewhere the attacker chose. This is an open redirect: untrusted input controls the destination of an HTTP redirect without a sufficiently strict destination policy.
The fix is not to become better at spotting suspicious URL strings. It is to make the set of acceptable destinations much smaller. For ordinary in-application navigation, accept only local application paths. When external redirects are genuinely required, map server-controlled identifiers to a small set of approved destinations or validate the parsed URL against an explicit policy.
A redirect crosses a trust boundary
A redirect response tells the client to make another request. In HTTP, a typical response looks like this:
HTTP/1.1 302 Found
Location: /accountThe browser interprets the Location field and navigates to the new location. A relative reference such as /account stays on the current origin. A complete URL can send the browser to another origin entirely.
That difference is the useful mental model. A redirect is not merely “which page comes next?” It is a decision about where the application tells the browser to place its next trust.
Suppose a login endpoint behaves conceptually like this:
POST /login?next=<destination>
if credentials are valid:
redirect to <destination>If <destination> is copied directly from the request, the application has delegated that trust decision to the requester. Authentication does not make the destination trustworthy. The parameter was untrusted before login and remains untrusted afterward.
An attacker does not need to compromise the server to abuse this behavior. They only need to choose a destination and persuade someone to follow the application URL. The trusted hostname at the beginning of the link can make the transition less obvious to the user.
The main threat addressed here is therefore untrusted external navigation through a trusted redirect endpoint, especially when it can support phishing or confuse security-sensitive flows. Redirect validation does not protect a user who independently chooses a malicious link, and it does not replace authentication, authorization, or protections against script injection.
Prefer a path when the feature only needs a path
Many redirect features do not need arbitrary URLs at all. A login flow usually wants to return a user to another page in the same application. Representing that requirement as an unrestricted URL gives the input more authority than the feature needs.
A smaller contract is easier to defend:
accepted: /projects/42
accepted: /settings/profile
rejected: external destination
rejected: malformed or ambiguous destinationThe exact rules depend on the application’s router and URL library, but the design goal is portable: if the destination should remain inside the application, accept a local path representation and reject inputs that can select another origin.
This is stronger than checking whether a string happens to contain your hostname. URL syntax has structure: scheme, authority, host, port, path, query, and fragment can affect how a destination is interpreted. String-prefix and substring tests reason about characters instead of that structure.
For example, a policy such as “the value starts with the company domain” is fragile because it mixes parsing and trust decisions. The application should first decide what form it accepts. If only local navigation is required, there is no reason to accept a scheme or host in the first place.
A useful failure behavior is also simple: when the supplied return target is invalid, redirect to a fixed safe page such as the application home or account dashboard. Do not try to repair an ambiguous value into something that might be acceptable.
Parse first when external URLs are necessary
Some systems legitimately redirect to other origins. A product may have several separately hosted applications, or an identity workflow may need to return to a known partner. In that case, “local paths only” is too restrictive.
The next safest design is usually to avoid accepting the external URL itself. Give each permitted destination a server-controlled identifier:
billing -> https://billing.example.net/continue
support -> https://support.example.org/startThe request carries billing, not an arbitrary URL. The server resolves that identifier from its own configuration. This makes the trust policy visible and keeps URL parsing out of the normal request path.
When the application truly must accept a URL, parse it with a URL parser appropriate to the platform, then validate the components that matter to the policy. For a typical HTTPS allowlist, that may include the scheme, normalized hostname, and port. Depending on the integration, the path may also need restrictions.
Conceptually:
candidate = parse(untrusted_value)
if candidate.scheme is not approved:
reject
if candidate.host is not an exact approved host:
reject
if candidate.port is not approved:
reject
redirect to candidateThis is teaching pseudocode, not a drop-in validator. URL parsing and normalization have platform-specific details, so production code should use the framework’s established URL and redirect APIs rather than hand-written splitting.
Exact host comparison is easier to reason about than substring matching. If subdomains are permitted, define that relationship deliberately rather than assuming that any hostname containing a trusted string is related to it. Likewise, approving https does not imply that every port or every path on an approved host is suitable for a security-sensitive flow.
Keep authorization separate from navigation
A redirect policy answers “where may the browser be sent?” It does not answer “what may this user do there?”
Imagine that /after-login accepts a local return path. Restricting the path to the application’s own origin is useful, but it would be a mistake to treat that check as authorization for the destination resource. If the return path is /admin/reports, the reports handler must still verify that the current user may view reports.
This separation matters because redirect and routing code often sits near authentication code. It is tempting to assume that reaching a destination through a trusted flow grants some extra authority. It should not. Every protected resource should enforce its own authorization at the appropriate boundary.
The same principle applies to server-side forwards and internal routing. Keeping a target local reduces one class of navigation risk; it does not make every local target appropriate for every principal.
Common fixes that leave the trust decision weak
Several approaches look like validation but preserve too much attacker control.
Checking whether the raw value contains an approved domain is one example. A URL is structured data, so a textual occurrence of a hostname does not establish which host a parser will use as the destination.
A broad regular expression can have the same problem. Regular expressions are useful for constrained string formats, but reconstructing general URL parsing rules in a pattern is difficult to review and easy to make inconsistent with the component that performs the redirect.
A denylist of known bad hosts is also the wrong shape for this problem. The set of destinations an attacker might control is effectively open-ended. The set of destinations your application intends to trust is usually small enough to define positively.
Another mistake is validating only on the client. Client-side checks can improve user experience, but a requester can call the server endpoint without using your page. The server that emits the redirect must enforce the destination policy.
Finally, do not assume that a redirect is harmless because it does not expose server data. Security failures can be compositional. A redirect endpoint may become useful to an attacker precisely because it borrows your application’s hostname and then hands the browser to another origin.
Decide how much flexibility the feature really needs
The right control depends on the redirect’s purpose.
For navigation within one web application, a local path is normally enough. Keep the representation local and reject anything outside that model.
For a small, stable set of external destinations, server-side identifiers are simpler to audit than accepting URLs. Configuration changes then become the place where new trust is introduced.
For a genuinely dynamic external integration, explicit parsed-component validation may be necessary. Treat the allowlist as security-sensitive configuration, test boundary cases, and make the fallback behavior fail closed to a known destination.
The more flexible the redirect becomes, the more operational work follows. Teams need to know who may add approved destinations, how stale entries are removed, whether control of an approved domain can change, and how redirect behavior is tested after configuration changes. An allowlist is only as trustworthy as the lifecycle of the entries in it.
Test the policy, not just the happy path
A redirect control is easy to verify with focused tests. Start from the promise the feature makes.
If the promise is “return targets remain on this application,” tests should show that ordinary local paths work and that values representing other origins, malformed destinations, and unsupported forms fall back to the fixed safe destination. The assertion should inspect the redirect target produced by the server, not merely whether the request returned a redirect status.
If external destinations are allowed, test every approved form and representative rejected forms around the policy boundaries: unapproved schemes, hosts, ports, and paths where those components are constrained. Tests should exercise the same parser and redirect mechanism used in production.
Logging rejected redirect targets can help detect misuse, but avoid recording sensitive query values unnecessarily. Detection is complementary; the primary defense is still refusing destinations outside the policy.
Keep the redirect’s authority narrow
The most useful question to ask when reviewing dynamic redirects is not “does this URL look suspicious?” It is “what destinations does this feature actually need permission to select?”
If the answer is pages in the same application, represent the input as a local path. If the answer is a few external services, resolve server-controlled names to fixed destinations. Accept arbitrary URLs only when the product requirement truly needs that flexibility, then parse and validate them against an explicit allowlist before redirecting.
That approach turns redirect safety from a string-filtering exercise into a small authorization decision over destinations. The narrower that decision is, the easier it is to explain, test, and keep correct.