Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Fixed
- **Upload Security Extractor False Positives** — refined `KEY_FROM_NAME`, `TRUST_CLIENT_MIME`, and `ACCEPT_SVG` regexes to stop flagging benign usage of originalname in string templates, common mimetype logging, and explicit SVG rejection conditions.


### Added

- **No-Row-Level-Security detection** (`missing-rls` class, in `schemas.py` + the ledger) — committed
Expand Down
15 changes: 10 additions & 5 deletions src/websec_validator/extractors/upload_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,16 @@
ALLOW_LIST = re.compile(r"isAllowedMediaType|allowedMimeTypes|allow[_-]?list|whitelist|ALLOWED_(?:MIME|TYPES|EXT)"
r"|ACCEPTED_(?:MIME|TYPES?|EXT)|accepted(?:Mime|File|Content)?(?:Types?|Extensions?)"
r"|\bfile-type\b|fileTypeFrom|magic[_-]?byte|detectContentType|\.fromBuffer\b|sniff", re.I)
KEY_FROM_NAME = re.compile(r"(?:Key|key|path|filename|filepath|destination|filename\s*\()\s*[:=(][^;\n]{0,90}"
r"\b(?:originalname|originalName|file\.name)\b"
r"|`[^`]*\$\{[^}]*\boriginalname\b[^}]*\}[^`]*`", re.I)
TRUST_CLIENT_MIME = re.compile(r"(?:req\.files?\.[\w$.]*\.|\bfile\.)mimetype\b|headers\[['\"]content-type['\"]\]", re.I)
ACCEPT_SVG = re.compile(r"image/svg\+xml|['\"]svg['\"]", re.I)
KEY_FROM_NAME = re.compile(r"(?:Key|key|path|filename|filepath|destination|filename\s*\()\s*[:=(]\s*"
r"(?:[^;{\n]{0,90}?)?"
r"(?:\b(?:originalname|originalName|file\.name)\b"
r"|`[^`]*\$\{[^}]*\b(?:originalname|originalName|file\.name)\b[^}]*\}[^`]*`)", re.I)
TRUST_CLIENT_MIME = re.compile(r"(?:===|!==|==|!=|\.includes\s*\(|[=:]\s*|switch\s*\(\s*|(?<!\blog)(?<!\binfo)(?<!\bwarn)(?<!\berror)(?<!\bdebug)\s*\(\s*[^);]*?)(?:req\.files?\.[\w$.]*\.|\bfile\.)mimetype\b"
r"|(?:req\.files?\.[\w$.]*\.|\bfile\.)mimetype\b\s*(?:===|!==|==|!=)"
r"|headers\[['\"]content-type['\"]\]\s*(?:===|!==|==|!=)", re.I)
ACCEPT_SVG = re.compile(r"\[[^\]]*(?:image/svg\+xml|['\"]svg['\"])[^\]]*\]"
r"|(?:\bincludes\b|\bindexOf\b)\s*\(\s*(?:['\"]image/svg\+xml['\"]|['\"]svg['\"])\s*\)"
r"|(?:===|==)\s*(?:['\"]image/svg\+xml['\"]|['\"]svg['\"])(?![^;]*?(?:throw|return|res\.(?:status|send|json)))", re.I)
# file-serving: streaming a STORED/PROXIED object back to the client. Tightened to genuine
# file-bytes sinks — the old rule matched a bare `getObject` token (a local coercion helper) and a
# Prometheus `res.set('Content-Type', registry.contentType)` (the /metrics endpoint), both FPs.
Expand Down
22 changes: 22 additions & 0 deletions tests/test_pentest_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,28 @@ def test_reuse_check_present_no_gap(self):


class UploadSecurityTests(unittest.TestCase): # N2
def test_upload_security_benign_fps(self):
# realistic, BENIGN code snippets that should NOT falsely trigger detections.
snippets = {
"fp1.ts": "res.status(400).json({ error: `File ${req.file.originalname} is bad` }); multer();",
"fp3.ts": "if (req.file.mimetype === 'image/svg+xml') throw new Error('reject'); multer();",
"fp4.ts": "console.log('Uploaded mime:', req.file.mimetype); multer();",
"fp5.ts": "res.setHeader('Content-Type', 'image/svg+xml'); res.send('<svg></svg>'); multer();"
}
ctx = repo(snippets)

# Test originalname FP
out1 = UploadSecurityExtractor().extract(repo({"fp1.ts": snippets["fp1.ts"]}), {})
self.assertNotIn("upload-key-from-filename", [f["kind"] for f in out1["findings"]], "Flagged benign originalname usage")

# Test SVG FP
out_svg = UploadSecurityExtractor().extract(repo({"fp3.ts": snippets["fp3.ts"], "fp5.ts": snippets["fp5.ts"]}), {})
self.assertNotIn("upload-accepts-svg", [f["kind"] for f in out_svg["findings"]], "Flagged benign SVG rejection/response")

# Test MIME FP
out_mime = UploadSecurityExtractor().extract(repo({"fp4.ts": snippets["fp4.ts"]}), {})
self.assertNotIn("upload-trusts-client-mime", [f["kind"] for f in out_mime["findings"]], "Flagged benign mimetype logging")

def test_unsafe_upload_and_serve_flagged(self):
out = UploadSecurityExtractor().extract(repo({
"upload.ts": "const u=multer({}); function uploadMedia(req,res){ if(isExecutableMimeType(req.file.mimetype)) return;"
Expand Down