VNX-938: Unvalidated redirect or forward to a request-supplied destination
Reports Python redirects whose destination is request data, and Java/Kotlin server-side forwards (getRequestDispatcher, Spring forward:) and Kotlin redirects built from request input without validation.
| Field | Value |
|---|---|
| Rule ID | VNX-938 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Python, Java, Kotlin |
| CWE | CWE-938 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:A/AL:A/IC:N/FC:LT/RP:N/RL:A/AV:I/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | open-redirect forward cwe-938 |
| Source | internal/sast/rules/vnx-938.rego |
Overview
VNX-938 maps to CWE-938: OWASP Top Ten 2013 Category A10 - Unvalidated Redirects and Forwards. A redirect to a user-supplied URL sends victims from a trusted domain to a phishing page; a forward to a user-supplied path runs inside the application and can serve pages the user’s role should not reach. This rule covers the shapes the per-language open-redirect rules do not: Python redirects, Java/Kotlin forwards, and Kotlin redirects (VNX-JAVA-007 covers Java redirects, and the Node.js, PHP and Go open-redirect rules cover those languages).
What Gets Flagged
return redirect(request.args.get("next", "/")) # flagged (Flask)
next_url = request.GET.get("next")
return HttpResponseRedirect(next_url) # flagged (Django)
String page = request.getParameter("page");
request.getRequestDispatcher("/WEB-INF/views/" + page + ".jsp").forward(request, response); // flagged
public String next(@RequestParam("step") String step) {
return "forward:" + step; // flagged
}
fun jump(@RequestParam target: String): String = "forward:$target" // flagged
response.sendRedirect(request.getParameter("url")) // flagged
Not flagged: a destination that starts with a fixed literal (redirect(f"/blog/{id}")), url_for/reverse lookups and named URL patterns, a value looked up in a constant map, a value checked first (url_has_allowed_host_and_scheme, urlparse(...).netloc, an allowlist membership test, containsKey/startsWith/equals), request attributes and other server-side values, comments and string literals. A fixed prefix on a forward path does not prevent ../ traversal, so it is still reported.
Remediation
Map the request value to a fixed set of destinations, or validate it against an allowlist:
from django.utils.http import url_has_allowed_host_and_scheme
if not url_has_allowed_host_and_scheme(next_url, allowed_hosts={request.get_host()}):
next_url = "/"
return HttpResponseRedirect(next_url)
References
Suppressing this rule
Put a nosec comment on the line, or on line 1 to cover the whole file. Use the comment marker of the file’s language (#, //, -- or ;):
# nosec vnx-938
// nosec vnx-938
Or record an ignore rule: vulnetix ignore add --rule vnx-938 --reason "...". See Suppressing findings.