VNX-186: Overly Restrictive Regular Expression
Reports denylist filters for script, markup or SQL injection tokens (<script, javascript:, onerror, UNION) that match case-sensitively, so <SCRIPT> or JavaScript: slips through: removals with an empty replacement, regexes without a case-insensitive flag, and membership tests of a bare token, in Python, JavaScript/TypeScript, Java, PHP and Ruby.
| Field | Value |
|---|---|
| Rule ID | VNX-186 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | Java, JavaScript / TypeScript, Python, PHP, Ruby |
| CWE | CWE-186 |
| CAPEC | CAPEC-79 |
| 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 | regex validation denylist xss cwe-186 |
| Source | internal/sast/rules/vnx-186.rego |
Overview
VNX-186 maps to CWE-186: Overly Restrictive Regular Expression. A filter that is meant to catch a dangerous value but only matches one spelling of it lets every other spelling through. The rule reports denylists for script, markup and SQL injection tokens that are matched case-sensitively: HTML, JavaScript URLs and SQL keywords are all case-insensitive, so <SCRIPT>, JaVaScRiPt: and UNION SELECT pass a filter written for the lower-case form.
What Gets Flagged
A pattern or literal naming <script, </script, javascript:, vbscript:, onerror, onload, onmouseover, <iframe, <svg, <img, <object, <embed, union, drop table, sleep( or benchmark(, used as:
- a removal with an empty replacement (
.replace,.replaceAll,.gsub,.sub,.delete,str_replace); - a regular expression with no case-insensitive flag (
re.sub/re.search/re.compilewithoutre.I, a JavaScript or Ruby/.../literal ornew RegExpwithouti,Pattern.compilewithoutCASE_INSENSITIVE,preg_replace/preg_matchwithout theimodifier); - a membership test of a bare token (
"<script" in value,.includes("<script"),.contains("union select"),strpos($s, "<script")).
name = name.replace("<script>", "").replace("</script>", "") # flagged
comment = re.sub(r"<script.*?>.*?</script>", "", request.form["c"]) # flagged
$name = str_replace('<script>', '', $name); // flagged
$msg = preg_replace('/<script(.*?)>/', '', $_POST['msg']); // flagged
return html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, ''); // flagged
return !/^javascript:/.test(u); // flagged
Not flagged: (?i), re.I/re.IGNORECASE, the i flag, Pattern.CASE_INSENSITIVE, str_ireplace/stripos, a subject lower-cased first (value.lower()), a replacement that inserts content (template assembly), regex extraction (.match, .exec, findall), a check for one exact payload, test code, minified files and comments.
Remediation
- Encode output for its context (HTML-escape, attribute-escape, URL-encode) or use a vetted sanitiser such as DOMPurify, bleach or OWASP Java HTML Sanitizer, instead of stripping tokens.
- For SQL, use parameterised queries; never filter keywords.
- If a denylist must stay as defence in depth, match case-insensitively and loop until the input stops changing.
import html
safe = html.escape(request.args.get("name", ""))
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-186
// nosec vnx-186
Or record an ignore rule: vulnetix ignore add --rule vnx-186 --reason "...". See Suppressing findings.