A redirect service takes a destination from a request and sends a browser somewhere else. If it accepts any destination without checking it, an attacker can turn your trusted domain into a stepping stone to a malicious site. That can make phishing links look legitimate and undermine trust in your application.
This tutorial explains how to prevent open redirect vulnerability in a redirect service. You’ll learn safer destination designs, how to validate relative paths and allowlisted URLs, which HTTP status codes to choose, and how open redirects differ from server-side request forgery (SSRF).
Key takeaways
- Prefer a server-side destination ID mapped to a fixed URL over accepting an arbitrary URL.
- For same-site redirects, accept only validated relative paths and use a configured application origin—not an origin derived from an untrusted
Hostheader. - For external destinations, compare parsed URLs against an explicit allowlist of exact hosts and permitted schemes.
- A redirect tells a browser where to go; it does not itself make the server fetch that destination. SSRF involves server-side network requests and needs separate protections.
- Choose redirect status codes based on whether the redirect is temporary or permanent and whether the request method must be preserved.
What is an open redirect?
A redirect endpoint might accept a destination in a query parameter:
https://app.example/redirect?to=https://destination.example/page
If the endpoint redirects to whatever value to contains, someone could replace the destination with an attacker-controlled site. A link that starts on a familiar domain can then lead to an unexpected destination after a click.
Open redirects can support phishing, make unsafe links appear more trustworthy, and interfere with security-sensitive flows when another system relies on a redirecting URL. The severity depends on the application and context; a redirect alone does not automatically expose credentials or bypass authentication.
A redirect endpoint is not the same as a URL fetcher. In a typical redirect, the server sends an HTTP response with a Location header, and the browser makes the next request. In SSRF, the application itself makes a network request to a user-influenced destination. Do not treat redirect validation as a substitute for SSRF defenses if your service also fetches URLs.
Choose a safe destination design
Prefer destination IDs
The simplest way to avoid accepting arbitrary destinations is to accept an identifier and look up the destination on the server:
https://app.example/go/docs
The application maps docs to a destination it controls. The identifier should refer only to an entry created or approved through a trusted process. Do not let an untrusted user create a mapping to any URL and then assume that using an ID makes it safe.
This approach is easy to audit and avoids many parsing edge cases. It is a strong default for links in onboarding, email, authentication, and other application workflows.
Use relative paths for same-site navigation
If users need to return to different pages on your site, accept a relative path such as /account rather than a complete URL. Resolve it against a fixed, configured application origin, then reject it unless it remains on that origin.
A value beginning with // is a network-path reference: browsers can interpret it as a URL on another host. Backslashes and control characters can also lead to confusing parsing or downstream behavior, so reject them rather than trying to repair the input.
Here is an illustrative Node.js helper for validating a same-origin path. Set APP_ORIGIN from trusted configuration, not directly from the incoming request:
const APP_ORIGIN = new URL("https://app.example").origin;
function safeRelativeDestination(input) {
if (typeof input !== "string" || input.length === 0 || input.length > 2048) {
return null;
}
// Require a root-relative path; disallow protocol-relative and backslash forms.
if (!input.startsWith("/") || input.startsWith("//") || input.includes("\\")) {
return null;
}
// Reject ASCII control characters rather than relying on parser normalization.
if (/[\u0000-\u001F\u007F]/.test(input)) {
return null;
}
try {
const target = new URL(input, APP_ORIGIN);
if (target.origin !== APP_ORIGIN || target.username || target.password) {
return null;
}
return `${target.pathname}${target.search}${target.hash}`;
} catch {
return null;
}
}
Use the validated result as the redirect target; reject invalid input or fall back to a safe, fixed page. This example is a starting point, not a universal validator. Test it against your framework, router, proxy, and browser behavior. If your application needs only a small set of return pages, mapping IDs to fixed paths is simpler still.
Validate external destinations with an allowlist
If the service genuinely needs to redirect to external sites, maintain an explicit allowlist. Parse the candidate with a standards-compliant URL parser and compare structured components—not a substring of the raw input.
A policy should specify:
- Allowed schemes. Usually permit only
https:unless there is a documented need for another scheme. Reject schemes such asjavascript:anddata:. - Exact hostnames. Compare the parsed hostname to an approved set, such as
partners.example. A suffix or substring test can accidentally accept a lookalike likepartners.example.attacker.test. - Ports. Permit only expected ports, usually the default HTTPS port, unless the use case requires otherwise.
- Credentials. Reject URLs containing a username or password before the host, such as
https://trusted.example@attacker.test/. - Normalization and matching. Define how host casing, internationalized names, trailing dots, and subdomains are handled. Do not allow every subdomain unless each one is under your control and covered by the policy.
For destinations with known parameters or paths, constrain those too. An approved host can still contain pages or query parameters that lead users into unwanted flows. Do not decode or normalize an input in one part of the application and validate a different representation elsewhere.
A useful rule is to reject by default and add destinations deliberately. Log rejected attempts in a way that supports investigation without storing unnecessary sensitive query data.
Use the right HTTP redirect status
Redirect status codes communicate how a client should treat the response. The choice affects caching and, for some codes, whether the client preserves the request method.
| Status | Typical use | Method behavior |
|---|---|---|
302 Found | Temporary redirect, common for browser navigation | Historically, clients may change a POST to GET; do not use it when method preservation is required. |
303 See Other | Send the client to a retrieval page after an action such as a form submission | Directs the client to retrieve the other resource, typically with GET or HEAD. |
307 Temporary Redirect | Temporary redirect when the original method and body must be preserved | Preserves the method. |
301 Moved Permanently | Permanent move | Can be cached; clients may change POST to GET. |
308 Permanent Redirect | Permanent move when the method must be preserved | Preserves the method and can be cached. |
For a temporary browser-facing redirect, 302 is common. For a post/redirect/get flow after a successful form submission, 303 is often appropriate. Use 307 or 308 when preserving the method is important. Avoid permanent redirects for destinations that may change, because clients and intermediaries can cache them. RFC 9110 defines the semantics of these status codes; framework behavior and client behavior should still be tested for your use case.
A practical implementation workflow
- Decide what users actually need. If they choose among known pages, use IDs mapped to fixed destinations. If they need same-site returns, accept relative paths. Allow external URLs only when the product requires them.
- Validate at the redirect boundary. Apply the destination policy immediately before creating the response, even if earlier code has already checked the input.
- Use a fixed origin. Do not construct trusted redirect URLs from an unvalidated
Hostor forwarded-host header. Configure the canonical origin and handle trusted proxy headers according to your deployment setup. - Fail safely. When validation fails, show an error or redirect to a fixed, safe page. Do not silently fall back to the original untrusted value.
- Set the response deliberately. Choose the status code based on permanence and method handling, and let your framework set the
Locationheader. Avoid manually concatenating untrusted strings into response headers. - Test edge cases. Include absolute URLs, protocol-relative paths, lookalike hosts, user-info syntax, unexpected ports, encoded characters, backslashes, empty values, and malformed input. Confirm the final
Locationvalue and behavior in the clients you support.
For a quick debugging aid, you can check a redirect URL. A checker can help inspect redirect behavior, but it cannot prove that your application’s validation policy is complete or replace code review and tests.
Common mistakes and pitfalls
- Checking whether a string contains an approved domain. A malicious URL can include the trusted hostname as part of a longer hostname or in its user-info section. Parse first and compare the hostname exactly.
- Allowing any URL with a familiar scheme. HTTPS is not enough; the host must also be approved.
- Using a weak suffix check. A rule like
hostname.endsWith("example.com")may acceptnotexample.com. If subdomains are intentionally permitted, check for a label boundary and document which subdomains are trusted. - Assuming a leading slash is always safe.
//other-host.example/pathbegins with slashes but can point to another host. Require a single root-relative slash and verify the parsed origin. - Trusting a URL parser as the whole policy. Parsing separates URL components; it does not decide which hosts, schemes, ports, or paths your application should allow.
- Confusing open redirects with SSRF. A browser redirect does not inherently cause the server to connect to the destination. If the server also fetches or previews the URL, implement separate SSRF protections, including controls on resolved addresses, redirects, and network egress.
- Using permanent redirects for changing destinations. Cached
301or308responses can continue directing users to an old location after your configuration changes.
Defensive recommendations
Keep the redirect policy centralized, small, and easy to test. Prefer fixed destination mappings; otherwise use a strict same-origin path rule or a narrowly scoped external allowlist. Keep the configured application origin separate from request-controlled headers, use framework response APIs, and review every feature that can influence the Location header.
Treat destination validation and SSRF prevention as separate controls. If a service fetches a URL, it needs additional server-side network protections; a hostname allowlist or browser redirect check alone is not sufficient.
Conclusion
A secure redirect service does not accept arbitrary destinations by default. Map trusted IDs to known URLs, validate same-site paths against a fixed origin, and use exact allowlists when external redirects are necessary. Choose HTTP status codes deliberately, test unusual URL forms, and keep SSRF defenses separate from redirect validation.
References
- OWASP, Unvalidated Redirects and Forwards Cheat Sheet.
- IETF, RFC 9110: HTTP Semantics, including the section on redirection status codes.
