An open redirect vulnerability (Nessus calls it CGI Generic Open Redirection) means a page reads a destination from a parameter such as returnUrl, next or url and sends the browser there without checking it. The fix is to accept only relative paths or allowlisted destinations, enforce that in application code, then retest with an external URL.
What the scanner is actually detecting
Nessus plugin 47834, CGI Generic Open Redirection, is in the CGI abuses family and rated Medium. “CGI” here means any server-side script or handler (PHP, ASP.NET, JSP, Python), not just files under /cgi-bin. Nessus sends specially crafted values to the parameters it discovered while crawling and reports those that make the application redirect to a third-party site. The plugin output names the script and the parameter, and those are your test cases.
Two details matter when you read the report. First, the plugin only runs when web application tests are enabled in the scan policy, so a basic network scan will never show it. Second, Tenable’s solution text says to “properly escape arguments”, which is misleading: escaping does not stop a redirect. The value has to be validated.
The same flaw (CWE-601) appears under other names in other tools:
| Scanner | Finding title | ID | Vendor severity |
|---|---|---|---|
| Nessus | CGI Generic Open Redirection | 47834 | Medium |
| Nessus | Open Redirect | 121040 | Medium |
| Tenable Web App Scanning | Unvalidated Redirection | 98054 | Medium (CVSS v3 4.7) |
| Burp Suite | Open redirection (reflected) | 0x00500100 | Low |
| ZAP | External Redirect | active rule 20019 | High (Location header variant) |
Plugin 121040 requires both web application tests and paranoid reporting (“Show potential false alarms” in the policy), so treat it as a lead to verify rather than a confirmed result. Tenable’s description of WAS 98054 covers more than 3xx Location headers: Refresh headers, HTML meta refresh tags and JavaScript redirects count too. The severity spread (Low to High) reflects vendor judgment about phishing impact, not different bugs.
How serious is an open redirect?
An open redirect does not give an attacker access to your server or its data. Its value to an attacker is borrowed trust:
- Phishing credibility: the link starts with your real domain and a valid certificate, so users and link filters trust it. Microsoft’s ASP.NET documentation describes the classic case: a login link whose
returnUrlpoints to a lookalike domain, so the victim signs in successfully, lands on a fake login page and types the password again. - OAuth and SSO token leakage: RFC 9700 (OAuth 2.0 Security Best Current Practice) notes that open redirectors on clients or authorization servers can enable exfiltration of authorization codes and access tokens. A redirector on a host registered as an OAuth redirect target deserves higher priority.
- Script execution in client-side redirects: if the page redirects with JavaScript, for example
window.location = value, ajavascript:value can turn the redirect into cross-site scripting.
For a plain server-side redirect on a page without login or SSO involvement, Medium or Low is a fair rating. Raise it for login pages, SSO callbacks and JavaScript-based redirects.
How to confirm it on the host
Only test applications you are authorized to test. Take the path and parameter from the plugin output and send a request with an external destination. curl does not follow redirects unless you pass -L, so you see the raw Location header:
curl -s -D - -o /dev/null 'https://app.example.com/login?returnUrl=https://example.org/' | grep -i '^location:'
A response of Location: https://example.org/ confirms the finding. Many applications block a full URL but still fall for protocol-relative or backslash variants, so test those too:
for p in 'https://example.org/' '//example.org/' '/%5Cexample.org/' '%2F%2Fexample.org/'; do
printf '%-22s ' "$p"
curl -s -D - -o /dev/null "https://app.example.com/login?returnUrl=$p" | grep -i '^location:' || echo '(no redirect)'
done
Read the Location value the way a browser would. Under the WHATWG URL Standard, browsers treat a backslash like a forward slash in http and https URLs, so Location: /example.org/ sends the user to example.org. Location: //example.org/ does the same. Also, the %{redirect_url} write-out variable in curl resolves Location with curl’s own parser, which does not follow browser backslash rules, so check the raw header instead.
If there is no Location header, look for redirects in the body, which Tenable’s WAS description also counts:
curl -s 'https://app.example.com/go?url=https://example.org/' | grep -iE 'refresh|location.|example.org'
On Windows, call curl.exe explicitly, because curl is an alias for Invoke-WebRequest in Windows PowerShell 5.1:
curl.exe -s -D - -o NUL "https://app.example.com/login?returnUrl=https://example.org/" | Select-String '^location:'
If returnUrl is only honored after a successful login POST, reproduce it in a browser with a test account.
How to fix it: validate returnUrl and every other redirect parameter
The fix lives in application code or framework configuration, not in the web server. In order of preference:
- Remove the parameter. Redirect to a fixed page (dashboard, home) after login and drop the user-supplied target entirely.
- Map IDs server-side. Accept a short key such as
?next=reportsand look up the real path in a server-side table. Unknown keys fall back to a default. - Accept only relative paths. The value must start with exactly one
/, the second character must not be/or, and it must not contain control characters (browsers strip tabs and newlines, so/<TAB>/example.orgbecomes//example.org). Then prepend your own base URL, as Tenable’s guidance for plugin 121040 recommends. - Allowlist hosts only when you truly need cross-domain redirects: parse with a URL parser, require
https, compare the exact hostname against a fixed set (never “ends with” or “contains”), and reject any value containing a backslash, because some URL parsers disagree with browsers about where the host ends.
| Decoded parameter value | Accept? | Reason |
|---|---|---|
| /account/settings?tab=2 | Yes | Single-slash relative path |
| https://example.org/ | No | Absolute URL to another host |
| //example.org/ | No | Protocol-relative, browser leaves your site |
| /example.org/ | No | Browser treats the backslash as a slash |
| javascript:alert(1) | No | Script URL, dangerous in client-side redirects |
Python (any framework)
BASE_URL = "https://app.example.com"
def safe_redirect_target(value, default="/"):
"""Return an absolute URL on our own host, or the default page."""
if (not value
or not value.startswith("/")
or value[1:2] in ("/", "\")
or any(ord(c) < 0x20 or c == "x7f" for c in value)):
return BASE_URL + default
return BASE_URL + value
Validate the value after the framework has decoded it once, then pass the result to your framework’s redirect function.
PHP
<?php
const BASE_URL = 'https://app.example.com';
function safe_redirect_target(?string $v, string $default = '/'): string {
if ($v === null || $v === '' || $v[0] !== '/'
|| (strlen($v) > 1 && ($v[1] === '/' || $v[1] === '\'))
|| preg_match('/[x00-x1Fx7F]/', $v)) {
return BASE_URL . $default;
}
return BASE_URL . $v;
}
header('Location: ' . safe_redirect_target($_GET['returnUrl'] ?? null), true, 302);
exit;
ASP.NET Core
The framework already implements the relative-path rule. Microsoft Learn documents LocalRedirect, which throws an exception for a non-local URL, and Url.IsLocalUrl, which lets you fall back gracefully:
private IActionResult RedirectToLocal(string returnUrl)
{
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
return RedirectToAction(nameof(HomeController.Index), "Home");
}
Replace every Redirect(userValue) call with this pattern or with LocalRedirect. Minimal APIs can use Results.LocalRedirect.
Django
The built-in LoginView only honors next when it points to request.get_host() or a host listed in success_url_allowed_hosts. Keep that set empty unless you need another host, and in custom views never pass request.GET["next"] straight to redirect().
If you cannot change the code yet
A WAF or reverse proxy rule that rejects a scheme or a leading // in the flagged parameter can reduce exposure for a few days, but encoding and backslash variants make such rules easy to bypass. Treat it as a stopgap, not a closure.
How to verify the fix and rescan
Rerun the curl loop. Every payload should now produce either no redirect or a Location on your own host (typically the default page). Then confirm legitimate use still works: returnUrl=/account/settings should still land on that page after login. Check every parameter the scanner listed, since fixing returnUrl on the login page does nothing for a url parameter on a download or logout handler.
Rescan with the same policy: web application tests enabled for 47834, plus paranoid reporting if you are closing 121040. For Tenable WAS, Burp or ZAP, rescan the specific URLs. An open redirect is runtime behavior, the kind of issue that a dynamic (DAST) scan confirms but code analysis alone cannot close, so a clean rescan is the proof.
What can break and how to roll back
- Legitimate cross-domain redirects: SSO return flows, partner portals and sibling subdomains (such as shop.example.com) stop working under a relative-only rule. Add them to an exact-host allowlist or an ID map.
- Absolute self-URLs: some applications build
returnUrlas a full URL of their own host. Users then land on the default page instead. Generate relative paths, or allowlist your own hostname. - Unhandled exceptions:
LocalRedirectthrows on non-local input; without handling, users see an error page instead of the default page. - Path prefixes: an application published under a subpath behind a reverse proxy needs that prefix in
BASE_URL.
Log rejected values for a week before tightening an allowlist, so you can see which destinations real users need (Microsoft’s guidance also recommends logging non-local URLs to help diagnose attacks). Rollback is a normal application redeploy of the previous build. Designing the fallback as “go to the home page” keeps the failure mode harmless.
Common false positive reasons
- Paranoid-only plugin: 121040 runs in a mode that deliberately includes potential false alarms. Confirm with curl before assigning work.
- Payload in the query string, not the host: a redirect to your identity provider such as
Location: https://login.example.com/authorize?returnUrl=https://example.org/contains the payload but does not leave your trust boundary. Check the host of the Location value. The identity provider must still validate that parameter. - Interstitial pages: a page that displays the destination and requires a click is not an automatic redirect. OWASP lists this pattern as a mitigation; document it.
- Stale or cached results: the scan ran before deployment, or a CDN served a cached 3xx response.
- Different virtual host: scanning by IP address can reach a default site that differs from the one you fixed.
FAQ
Is an open redirect a high-severity vulnerability?
Usually not on its own. It becomes more serious on login pages, in OAuth or SSO flows where it can leak tokens, and in JavaScript redirects where a javascript: URL can run script.
Does URL-encoding or escaping the parameter fix Nessus 47834?
No. The browser follows the decoded destination. Only validation (relative path, ID mapping or exact-host allowlist) removes the unvalidated redirection.
Why does the scanner still flag a parameter that only accepts paths?
Most often the check tests for a leading / but still accepts //host or /host. Reject a second character of / or .
Which parameters should I check?
Every one the scanner lists, plus similar names in the same codebase: returnUrl, next, url, redirect, goto and continue are common.
Tracking this finding across many hosts
Open redirects tend to surface in several scanners at once, and the reports do not line up. SITEY, a self-hosted vulnerability management platform, imports findings from 16 scanners (Nessus results arrive as an uploaded .nessus export) and merges duplicates within each scanner, not across scanners, so a Nessus and a Burp record for the same parameter stay separate. For Nessus and Burp findings it can run a per-finding retest to confirm the redirect is gone before closing the item.