Open Redirect Scanner
A login or callback URL that sends the user wherever a parameter says turns your domain into a phishing launcher. The scanner finds parameters that look like redirect targets, puts an attacker URL in them and checks whether the server obeys.
Which endpoints get tested
An open redirect needs a place where a URL goes in and a redirect comes out. The scanner does not try every endpoint. It selects the ones that look like they accept a destination, by parameter name or by path, and tests each (endpoint, parameter) pair.
1Discovery produced a list of endpoints. Most of them have nothing to do with redirects, and sending payloads at them would only add noise.
2Parameter name. A query parameter, in the URL or declared by discovery, is selected if its lower-cased name is one of 17: redirect, url, next, return, returnto, continue, goto, dest, destination, redir, redirect_uri, callback, to, returnurl, return_url, forward, location.
3Path. A path that contains /login, /logout, /auth/callback, /oauth/authorize, /sso, /signin or /signout is tested with the parameter redirect, even when no parameter is visible yet.
4Everything else is left alone. There is no endpoint cap, but the scan stops cooperatively when its time budget runs out.
Probing and judging the response
Each selected parameter receives up to four payloads, in order. The client does not follow redirects, so the scanner sees the server's own first answer instead of wherever it points.
1Payloads are sent one at a time into the redirect parameter: an absolute URL, a protocol-relative URL, a backslash trick and a null-byte variant. Redirects are not followed, so the first response is the evidence.
2Header check. The status must be 3xx and a Location header must be present. The location counts as external if its host contains evil.com, whether written as an http(s) URL, //evil.com, or the browser-normalised forms /\/evil.com and ///evil.com. A // redirect to the site's own host is not external. That is HIGH, confidence 0.90.
3Body check. If the header check found nothing, the lower-cased body is searched for window.location =, window.location.href =, window.location.replace( or a meta refresh, each pointing at http(s)://evil.com. That is MEDIUM, confidence 0.75.
4A redirect to the app's own page is silent. The check looks for the scanner's own marker host, so a redirect to some other external host is not reported either. The first hit stops the loop: one finding per parameter.
Why it matters
A link on your domain that bounces to an attacker's page borrows your reputation. A user sees yourapp.com/auth/callback?redirect_to=evil.com/fake-login, trusts the domain and types credentials into the copy. The same flaw can be chained with OAuth flows to steal authorization codes, or used to deliver malware downloads from a trusted domain.
Google's redirect endpoints (google.com/url?q=...) have been repeatedly abused in phishing campaigns to make malicious links appear to originate from google.com.
Vulnerable vs fixed code
// BAD: whatever arrives in redirect_to is where the user lands
const redirectTo = searchParams.get("redirect_to") || "/"
return NextResponse.redirect(redirectTo)// GOOD: resolve against your own origin, then compare origins
const redirectTo = searchParams.get("redirect_to") || "/"
const target = new URL(redirectTo, request.url)
if (target.origin !== new URL(request.url).origin) {
return NextResponse.redirect(new URL("/", request.url))
}
return NextResponse.redirect(target)A fix that only rejects values starting with http or // is not enough. The scanner's third payload, /\/evil.com, starts with neither, and browsers treat the backslash as a slash. Resolving the value with new URL and comparing origins covers all four payload shapes the scanner sends, and the markdown fix uses the same approach.
Limits and what it needs
| Check | Reads | Reports when | Severity · confidence |
|---|---|---|---|
| Header redirect | status and Location | 3xx and the Location host (http(s), //, or backslash forms) contains evil.com | High · 0.90 |
| Body redirect | response body, lower-cased | JavaScript location assignment, replace( or meta refresh to http(s)://evil.com | Medium · 0.75 |
Only GET requests are sent, and only to parameters and paths that look like redirects, so a destination hidden in a POST body or in a parameter named something else is not tested. Redirects through an unusual hostname are invisible to the check because it looks for evil.com specifically. Credentials are optional: with them, the probes carry the scan's auth headers.