diff --git a/CHANGELOG.md b/CHANGELOG.md index db53af9..6281a29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Fixed +- **Upload Security False Positives** in `upload_security.py` where static file serving via `res.sendFile(__dirname)` or literal paths, and valid allowlist checking using variable names like `validTypes`, falsely triggered alerts. Also tightened the `image/svg+xml` check to avoid generic `'svg'` strings. + ### Added - **No-Row-Level-Security detection** (`missing-rls` class, in `schemas.py` + the ledger) — committed diff --git a/src/websec_validator/extractors/upload_security.py b/src/websec_validator/extractors/upload_security.py index dc0c2ba..fc62706 100644 --- a/src/websec_validator/extractors/upload_security.py +++ b/src/websec_validator/extractors/upload_security.py @@ -24,17 +24,19 @@ # `ACCEPTED_*` / `acceptedMimeTypes` is the same intent under a different name (was missed → FP). 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"|(?:valid|supported)(?:Mime)?Types" 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) +ACCEPT_SVG = re.compile(r"image/svg\+xml", 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. SERVE_FILE = re.compile(r"res\.sendFile|\.sendFile\s*\(|\.getObject\s*\(|createReadStream|proxyMedia" r"|streamObject|\.pipe\s*\(\s*res\b|fs\.createReadStream", re.I) +STATIC_SERVE_CONTEXT = re.compile(r"__dirname|process\.cwd\(\)") NOSNIFF = re.compile(r"nosniff", re.I) # `Content-Disposition: attachment` fully defeats the MIME-sniff→stored-XSS vector (the browser # downloads instead of rendering), so a serve site that sets it is SAFE even without nosniff. @@ -76,7 +78,19 @@ def extract(self, ctx: RepoContext, facts: dict) -> dict: findings.append({"severity": "MEDIUM", "kind": "upload-accepts-svg", "file": rel, "detail": "`image/svg+xml` is accepted — SVG can carry inline