VNX-646: Uploaded file accepted or classified by its client-supplied name or extension
Reports upload handlers (multer, formidable, express-fileupload, Flask/Werkzeug, FastAPI, Django request.FILES and FileExtensionValidator) that decide on a file from the extension of the name the client sent, in a file that never inspects the content.
| Field | Value |
|---|---|
| Rule ID | VNX-646 |
| Severity | Medium |
| SARIF level | warning |
| Kind | sast |
| Languages | JavaScript / TypeScript, Python |
| CWE | CWE-646 |
| CAPEC | CAPEC-66 |
| ATT&CK | T1190 |
| CWSS vector | CWSS:1.0/TI:M/AP:RU/AL:S/IC:N/FC:LT/RP:RU/RL:S/AV:L/AS:N/IN:A/SC:A/BI:M/DI:H/EX:M/EC:N/P:C |
| Tags | file-upload file-extension cwe-646 |
| Source | internal/sast/rules/vnx-646.rego |
Overview
VNX-646 maps to CWE-646: Reliance on File Name or Extension of Externally-Supplied File. It reports a condition, return or membership test that looks at the extension of an uploaded file’s client-supplied name (originalname, originalFilename, req.files.x.name, .filename), directly or through a variable derived from it, when nothing in the same file checks the file’s content.
Why This Matters
The client picks the file name. shell.php.jpg, an executable renamed to invoice.pdf or an HTML page called avatar.png passes an extension check. If the server then stores, serves or parses the file according to that name, the attacker chooses how it is treated.
What Gets Flagged
if (!/\.(jpe?g|png|gif)$/i.test(file.originalname)) { return cb(new Error('images only')); }
const ext = path.extname(req.file.originalname).toLowerCase();
if (!['.png', '.jpg'].includes(ext)) { ... }
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
validators=[FileExtensionValidator(allowed_extensions=["pdf", "docx"])]
Not flagged: the same checks in a file that also sniffs the content (fileTypeFromBuffer, magic.from_buffer, Image.open(...).verify()), building a storage name such as Date.now() + path.extname(file.originalname), and extension checks on paths that are not uploads.
Remediation
- Determine the type from the content: magic bytes (
file-type,python-magic) or a decoder that rejects anything else (Pillowverify()). - Store uploads under a server-generated name and serve them with a server-chosen
Content-TypeandContent-Disposition: attachment. - Keep extension allow-lists as a usability check, never as the security check.
References
- CWE-646: Reliance on File Name or Extension of Externally-Supplied File
- OWASP File Upload Cheat Sheet
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-646
// nosec vnx-646
Or record an ignore rule: vulnetix ignore add --rule vnx-646 --reason "...". See Suppressing findings.