VNX-641: Improper Restriction of Names for Files and Other Resources
Detects upload file names classified by something other than their final extension: the extension taken from the first dot (split('.')[1], indexOf('.'), explode('.')[1]) or an allowlist tested as a substring ('.png' in name, includes('.jpg'), contains(".pdf"), unanchored regexes), so shell.php.jpg passes. Java, JavaScript/TypeScript, Python and PHP.
| Field | Value |
|---|---|
| Rule ID | VNX-641 |
| Severity | Low |
| SARIF level | note |
| Kind | sast |
| Languages | Java, JavaScript / TypeScript, Python, PHP |
| CWE | CWE-641 |
| 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 | file-upload double-extension file-name cwe-641 |
| Source | internal/sast/rules/vnx-641.rego |
Overview
VNX-641 maps to CWE-641: Improper Restriction of Names for Files and Other Resources. A file name is restricted by the part of it that decides how the server treats the file: the text after the last dot. Code that looks anywhere else lets a double extension through: avatar.jpg.php contains .jpg, and shell.png.php has png after its first dot, yet both are stored and served as PHP.
Reported, on a file-name expression (filename, fileName, fname, originalname, f.filename, file.name, getOriginalFilename(), getSubmittedFileName(), $_FILES[...]['name'], or a variable assigned straight from one in the 15 lines above):
- The extension taken from the first dot: Python
split('.')[1],split('.', 1)[1],partition('.'); JavaScript and Javasplit('.')[1]/split("\\.")[1]andsubstring/substr/slicefromindexOf('.'); PHPexplode('.', $name)[1],substr($name, strpos($name, '.')),strstr($name, '.'). - An allowlist tested as a substring: Python
'.png' in nameorany(ext in name for ext in ALLOWED); JavaScriptincludes('.jpg')orindexOf('.jpg') !== -1; Javacontains(".pdf")orindexOf(".pdf") >= 0; PHPstrpos/stripos/strstr($name, '.jpg'); and an extension regex with no$anchor in JavaScript.test()/.match()or PHPpreg_match().
What Gets Flagged
ext = f.filename.split(".")[1].lower() # FLAGGED: first dot
if ".png" in f.filename: # FLAGGED: substring
if (file.originalname.includes('.jpg')) return cb(null, true); // FLAGGED
if (/\.(pdf|docx)/i.test(file.originalname)) return cb(null, true); // FLAGGED: no $ anchor
$ext = explode('.', $name)[1]; // FLAGGED
What Is Not Flagged
- The final extension:
rsplit('.', 1),split('.')[-1],split('.').pop(),lastIndexOf('.'),os.path.splitext,path.extname,pathinfo,FilenameUtils.getExtension. endswith/endsWithchecks and$-anchored patterns.- The same calls on values that are not file names (versions, host names, e-mail addresses, JWTs), comments and string literals.
Storing an upload under the client’s name is VNX-66; a case-sensitive extension check is VNX-178.
Remediation
Take the final extension, lower-case it, compare it with an allowlist, and store the file under a generated name:
ext = os.path.splitext(f.filename)[1].lower()
if ext not in {".png", ".jpg", ".gif"}:
abort(400)
f.save(os.path.join(UPLOADS, f"{uuid.uuid4().hex}{ext}"))
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-641
// nosec vnx-641
Or record an ignore rule: vulnetix ignore add --rule vnx-641 --reason "...". See Suppressing findings.