Skip to content

VNX-1289: Improper Validation of Unsafe Equivalence in Input

Detects an uploaded file name checked against a denylist of dangerous extensions (php, exe, jsp, html, svg...) with a case-sensitive comparison, so shell.PHP passes. Python and JavaScript/TypeScript.

Updated 02 Nov 2026Checked against cli@de9b52a
Field Value
Rule ID VNX-1289
Severity Low
SARIF level note
Kind sast
Languages Python, JavaScript / TypeScript
CWE CWE-1289
CAPEC CAPEC-66
ATT&CK T1190
CWSS vector CWSS:1.0/TI:L/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:L/DI:H/EX:L/EC:N/P:L
Tags input-validation-inconsistent cwe-1289
Source internal/sast/rules/vnx-1289.rego

Overview

VNX-1289 maps to CWE-1289: Improper Validation of Unsafe Equivalence in Input. Input that is equivalent to a dangerous value, but not byte-identical to it, slips past a check written for the exact form. A file-upload denylist that compares the name case-sensitively is the most common case: the check rejects shell.php, while the web server still runs shell.PHP.

Reported: on an upload name (Flask/Werkzeug .filename, Django request.FILES[...].name (also f.name after f = request.FILES[...]), multer/formidable originalname/originalFilename, req.file(s) .name), directly or through a variable assigned from it in the 15 lines above (including os.path.splitext and path.extname results):

  • .endswith()/.endsWith() with a dangerous extension literal (php, phtml, phar, exe, bat, cmd, sh, jsp, asp/aspx, py, pl, cgi, js, html/htm/shtml/svg, dll, jar/war, ps1, vbs, hta, htaccess and similar), including a tuple split over lines;
  • membership in an inline or module-level list, tuple or set that holds such an extension (in, .includes(), .has());
  • ==/=== against one.

Not reported: a name lowered first (.lower(), .casefold(), .toLowerCase()), allowlists of harmless extensions, extension tests on names that are not uploads (such as os.walk results), comments and strings. Other unsafe equivalences (Unicode normalisation, trailing dots, alternate path separators) are not detected.

What Gets Flagged

f = request.files["file"]
if f.filename.endswith(".php"):   # FLAGGED: "shell.PHP" passes
    abort(400)
const ext = path.extname(req.file.originalname);
if (['.php', '.jsp', '.html'].includes(ext)) {   // FLAGGED
  return res.status(400).end();
}

What Is Not Flagged

ext = os.path.splitext(f.filename)[1].lower()
if ext in [".exe", ".php"]:
    abort(400)

Remediation

  1. Normalise (lower-case) the name before comparing it.
  2. Prefer an allowlist of permitted extensions and content types over a denylist.
  3. Store uploads outside the web root under generated names.

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-1289
// nosec vnx-1289

Or record an ignore rule: vulnetix ignore add --rule vnx-1289 --reason "...". See Suppressing findings.

Edit this pageSources: cli/internal/sast/rules/vnx-1289.rego, cli/internal/sast/docs/vnx-1289.md
esc
↑ ↓ move↵ openSearches in your browser. A key like autonomy.max or a code like threshold_required jumps straight to it.