A redirect to an attacker domain is reachable - the same mechanism leaking an OAuth authorization code to that domain is account takeover. Severity lives in the chain, not the 302.
A redirect parameter that accepts any URL looks like a low-severity UX mistake. What actually decides severity is the chain: an unconstrained redirect to an attacker-controlled host is a phishing aid. That same redirect carrying an OAuth authorization code to the attacker's server is account takeover that requires no user interaction once the code is in flight. The 302 is the mechanism; the chain is the finding.
Open redirects appear wherever an application routes users to different destinations after a shared step - login, logout, payment confirmation, email verification. A next, redirect_uri, returnTo or goto parameter is set by the caller, echoed into a Location header, and the browser follows it without inspection. Most frameworks produce this pattern naturally; most code reviewers skip it because the redirect appears intentional.
Confirming a bare redirect requires only a controlled hostname in the Location header. Confirming an OAuth chain requires that the authorization code actually appears in the URL delivered to that host. Those are two different findings, two different confirming artifacts, and two different severities - and a scanner that stops at the first 302 conflates them.
An open redirect rarely stops at a 302. The chains that escalate severity from low to critical share one property: the attacker collects the sensitive value without needing to interact with the victim's live session after the redirect fires. The value arrives in the URL before the user sees any attacker-controlled page.
| Chain | Mechanism | Confirming artifact | Severity |
|---|---|---|---|
| OAuth code redirect | redirect_uri not validated against registered list; authorization server issues code to attacker-controlled host | code= parameter received at nonce host in the Location redirect | Critical |
| Password-reset hijack | reset link built from a redirect parameter rather than a pinned base URL; one-time token travels to attacker host | token= parameter present in the redirect URL logged at nonce host | Critical |
| SSRF filter bypass | SSRF allowlist permits redirects from a trusted domain; open redirect on that domain forwards to an internal or metadata target | internal service or metadata response received through the chain | Critical |
| Bare phishing redirect | victim lands on attacker page after authentication; no sensitive value travels in the URL itself | attacker host receives the request; no secret value in path or query | Low-Medium |
The probe uses a nonce endpoint under a domain the tester controls, which logs incoming requests, rather than real attack infrastructure. For a bare redirect the confirming artifact is the Location header. For an OAuth chain it is the authorization code that appears in the redirect URL when the flow completes with the tester's own test account - no victim credential is used, no real exchange runs.
# step 1: start the OAuth flow; redirect_uri points to the nonce host GET /oauth/authorize ?client_id=YOUR_CLIENT &redirect_uri=https://probe-62615533.aptest.internal/cb &response_type=code &state=nonce-abc Host: auth.target.example # step 2: tester authenticates with their own account; observe the 302 HTTP/1.1 302 Found Location: https://probe-62615533.aptest.internal/cb?code=AUTHZ_CODE_HERE&state=nonce-abc # authorization code arrived at the nonce host # a real attacker exchanges it immediately for an access token
For the SSRF bypass chain, the probe sets the SSRF target to the open-redirect URL on the trusted domain, which in turn points to a benign nonce listener on an internal address. If the listener receives the request, the trusted-domain filter was bypassed without targeting a real internal service. No metadata endpoint is probed; only the redirect path is confirmed.
A bare redirect is low to medium: it is a phishing aid that requires social engineering to convert. The moment a sensitive value travels in the URL to an attacker-controlled domain the severity changes - the attacker collects it without any real-time interaction with the victim after the initial click. The finding is rated at the confirmed chain, not at the redirect mechanism.
The fix is a server-side allowlist of permitted redirect destinations - not a blocklist of known-bad domains. A blocklist misses every domain registered after the check was written. For OAuth, the redirect_uri must match an exact registered value before the authorization code is issued. For post-login redirects, the application should derive the destination from a local path identifier rather than from a user-supplied absolute URL.
URL parsing differences across frameworks create allowlist bypass paths that are stack-specific. A redirect_uri of https://trusted.example.com@attacker.example.com sends the browser to attacker.example.com - the @ makes everything before it the userinfo field, not a hostname. Encoded slashes (%2F), Unicode normalization of lookalike characters, and open-path fragments (#) that a naive string-prefix check accepts produce similar bypasses. The allowlist check must parse the URL with the same parser the runtime uses, normalize it, and compare only scheme, host and port against the registered list - not the raw string the caller supplied.