auditing-open-redirect-and-forced-navigation · git:20260903.6dac719 · 2026-09-03 · sha256 19ea0aa76c85037e
auditing-open-redirect-and-forced-navigation git:20260903.6dac719A
Immutable. This exact content is served forever at /api/v1/blob/19ea0aa76c85037e.
--- name: auditing-open-redirect-and-forced-navigation description: >- Audit redirect and navigation flows where an untrusted return, next, or callback URL, a stored value, or a referer drives a server redirect, a client-side location assignment, or a meta refresh to an attacker-chosen destination, enabling phishing, credential capture on a lookalike page, or theft of an OAuth code or token carried on the redirect. Use when a parameter or stored value controls where a user is sent after login, logout, an action, or an authorization step. Covers protocol-relative and backslash hosts, userinfo and whitespace tricks, and dangerous schemes that a naive allowlist misses. The untrusted target URL is the source, the redirect or navigation is the sink, and sending the user to an attacker-controlled destination is the bug. license: MIT --- # Auditing open redirect and forced navigation: when the app sends the user wherever the URL says An application constantly sends users onward: back to where they were after login, to a success page after an action, to an identity provider and back during authorization. When the destination comes from an untrusted parameter, a stored value, or a header, and the app redirects there without confirming it is one of its own pages, the app becomes a trusted springboard to an attacker's site. The link starts on the real domain, so it survives a glance and a mail filter, and lands on a lookalike login that harvests credentials. Worse, when the redirect carries an authorization code or a token in the URL, forcing the destination steals that secret outright. The bug is not the redirect feature; it is a destination taken from the request and trusted without an allowlist. You find it by locating every redirect and navigation and asking whether an attacker chooses where it goes. ## When to use - A return, next, redirect, callback, or continue parameter controls where a user is sent after an action. - Client code assigns a location or writes a meta refresh from a value influenced by the request or storage. - An authorization or single-sign-on flow returns to a URL supplied in the request and carries a code. ## Scope check Test redirect behavior only against applications you own or are authorized to assess, sending the redirect to a benign destination you control rather than a live phishing page, and never capturing a real user's token. A confirmed forced redirect that carries a token is credential theft, so keep every probe within the authorized scope. If you can't name the authorization, stop. ## The loop 1. **Establish whether the target is checked against an allowlist first.** For each redirect, determine whether the destination is confirmed to be one of the app's own pages, by an allowlist of paths or hosts matched exactly, or whether it is used as given. This is the false-positive killer: a redirect that only ever sends the user to a fixed internal path, or validates the target against an exact allowlist, is not open, while one that reflects the parameter is. Name the check before crafting a target. 2. **Map every redirect and navigation fed untrusted input.** Trace return and next parameters, stored redirect values, and referer or other headers into server redirects, client-side location assignments, and meta refreshes, including the return URL of any authorization flow. Each is a candidate sink. 3. **Test the bypasses a naive check misses.** A check that requires the target to start with a slash is beaten by a protocol-relative slash-slash-host and by a backslash the browser treats as a slash; a host allowlist is beaten by a userinfo at-sign, an embedded credential, a lookalike or encoded host, and trailing whitespace or control characters. Decide which of these the check as written lets through. 4. **Judge the destination scheme.** A redirect that permits a script or data scheme in a client-side navigation is not only off-site but executes in the current origin; confirm whether the sink restricts to HTTP and HTTPS or passes any scheme, since the impact differs from a plain off-site send. 5. **Follow what the redirect carries.** A plain open redirect is a phishing primitive; a redirect in an authorization flow carries a code or token in the URL or fragment, so forcing its destination steals that secret. Determine whether the sink is a bare navigation or one that transports a credential, because that sets the severity. 6. **Confirm and record.** Confirm by supplying a target that resolves to a benign host you control and observing the app send the user there, and for authorization flows, the code or token arriving at your host, on an isolated instance. Kill the lead if the target is matched against an exact allowlist of the app's own pages, if only a fixed internal path is used, or if no untrusted value reaches the redirect. Record the parameter, the sink, the bypass used, and whether a credential was carried, or set a `kill_reason`. ## Where forced navigation leaks - **The allowlist is the whole defense.** A destination confirmed to be one of the app's own pages cannot be redirected off-site; a reflected parameter can. The presence and exactness of the allowlist is the finding. - **Starts-with-slash is not enough.** A protocol-relative slash-slash-host and a backslash-host both pass a naive leading-slash check and send the user to another origin. - **Host checks fall to userinfo and lookalikes.** An at-sign puts the real host in the userinfo and the attacker host after it, and an encoded or homoglyph host defeats a substring match; only an exact host comparison holds. - **Schemes matter on the client.** A client-side navigation that accepts a script or data scheme executes in the origin, turning an open redirect into script execution rather than a mere off-site send. - **Authorization redirects carry secrets.** When the redirect transports a code or token, controlling the destination hands that credential to the attacker, which is far worse than phishing. ## Worked example (a confirm and a kill) > **Confirm.** A login flow reflects a return parameter into the post-login redirect after only checking that > it begins with a slash. A value of backslash-backslash-host slips past the check, and the app sends the > authenticated user, carrying the session-establishing fragment, to a host the tester controls on an > isolated instance. **Confirmed** open redirect carrying a credential to an attacker host, `high`, > remediation = validate the return target against an exact allowlist of the app's own paths, reject > protocol-relative, backslash, userinfo, and non-HTTP targets, and never place a code or token on a redirect > whose destination is client-influenced. > > **Kill.** The same flow resolves the return target against an allowlist of the app's own relative paths, > rejects any absolute URL, protocol-relative or backslash form, userinfo, and non-HTTP scheme, and falls > back to a fixed internal path when the target is not on the list. A crafted off-site target is discarded and > the user lands on the default page. **Killed**, `kill_reason` = "redirect target is matched against an exact > allowlist of the app's own paths and every off-site or alternate-scheme form is rejected; the destination is > never attacker-chosen and carries no credential off-site." ## Rationalizations to reject - *"It only redirects within our site."* -> Confirm that with an exact allowlist; a leading-slash or substring check is bypassed by protocol-relative, backslash, and userinfo forms that leave the site. - *"An open redirect is low severity."* -> On an authorization flow it steals the code or token, and anywhere it is a credible phishing springboard from your trusted domain; rate it by what it carries. - *"We block http and https other hosts."* -> Also block protocol-relative and backslash hosts and non-HTTP schemes; the bypasses live in the forms a naive host check does not parse. - *"The parameter is validated."* -> Validated how; a check that the value is a URL or starts with a slash is not a check that it is one of your pages. Only an exact allowlist answers that. - *"Users can see the address bar."* -> The link begins on your domain and the redirect is instant; the user and the mail filter both trust the starting host, which is the whole point of the abuse. ## Executing this in practice You need every redirect and client-side navigation fed an untrusted target, the check applied to each, and whether the sink carries a code or token. For each, decide whether an allowlist confines the destination to the app's own pages or a bypass reaches another origin, and whether the scheme is restricted. Reading the validation settles most leads; supplying a target that resolves to a benign controlled host, and watching for a carried credential, on an isolated instance settles the rest. ## Related - `hunting-crlf-and-response-splitting` - the Location header is the shared sink; a redirect target can also carry a CRLF, so the two analyses meet on the same header. - `hunting-host-header-and-url-parsing-trust` - the parser differentials and host-confusion tricks that beat a redirect allowlist are the same ones that skill treats for URL parsing trust. - `hunting-unicode-normalization-and-canonicalization-bypass` - an encoded or homoglyph host that defeats the redirect allowlist is a canonicalization failure that skill analyzes directly. - `hunting-reflected-and-stored-xss` - a client-side navigation that accepts a script or data scheme turns an open redirect into the script execution that skill hunts. - [FINDING-SCHEMA.md](../../FINDING-SCHEMA.md) - source = the untrusted target URL, sink = the redirect or navigation, evidence = the user sent to a benign controlled host, or a carried code or token arriving there, on an isolated instance.