From 8b65fd4350e60e90ffdcb611aefc8c29d0b487e6 Mon Sep 17 00:00:00 2001 From: Satendra Rawat Date: Fri, 25 Sep 2026 17:02:56 +0530 Subject: [PATCH] fix(malware-scan): cut false positives from vendor bundles and require() calls WHITESPACE_PADDING is now low outside config files when nothing code-like is hidden after the padding and either the hidden text is short or the line is a comment (aligned JSDoc/tables in vendor bundles and .d.ts files, e.g. Elementor's " * @param {Object}<155 spaces>options"). Low is hidden by default. It stays high when code-like tokens (require(, global[, eval(, fromCharCode, _$_xxxx, URLs) or 200+ characters of non-comment text are hidden, and always high in config files. GLOBAL_REQUIRE no longer matches storing a require() call result (global.performance = require("perf_hooks")); it still matches assigning the require function itself (global['r'] = require), as the payload does. Checked against 10 real infected samples (all still 4-6 high, same rules) and 3 false-positive sets (Elementor AI bundles, Figma .d.ts, Moodle ogv.js WASM decoders: 20 high -> 0 high). kdn-website: 12 high -> 0 high. Co-Authored-By: Claude Opus 5.5 --- malware-scan/README.md | 4 ++-- malware-scan/scan_malicious.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/malware-scan/README.md b/malware-scan/README.md index ef4694e..6ca04ce 100644 --- a/malware-scan/README.md +++ b/malware-scan/README.md @@ -20,8 +20,8 @@ objects without checking anything out. | Rule | Severity | Catches | |------|----------|---------| -| `WHITESPACE_PADDING` | high | Code after 150+ spaces/tabs on one line (hidden off-screen) | -| `GLOBAL_REQUIRE` | high | `require` assigned onto `global` | +| `WHITESPACE_PADDING` | high in config files, or when code (200+ chars, or `require(`, `global[`, `eval(` and similar) follows; otherwise low | Code after 150+ spaces/tabs on one line (hidden off-screen). Aligned comments in vendor bundles and `.d.ts` files are low, so hidden unless you pass `--min-severity low` | +| `GLOBAL_REQUIRE` | high | The `require` function itself assigned onto `global` (`global['r'] = require`). Storing the result of a call, like `global.x = require('y')`, is not flagged | | `GLOBAL_MARKER` | high | The campaign's string marker assigned onto `global` | | `OBFUSCATOR_ID` | high | `_$_` + 4 hex identifiers from the obfuscator | | `LIFECYCLE_SCRIPT` | high | `preinstall`/`postinstall`/`prepare` npm scripts that download or pipe to a shell | diff --git a/malware-scan/scan_malicious.py b/malware-scan/scan_malicious.py index 8f4d77d..a5ac336 100755 --- a/malware-scan/scan_malicious.py +++ b/malware-scan/scan_malicious.py @@ -48,7 +48,7 @@ ("WHITESPACE_PADDING", "high", re.compile(r"\S[ \t]{150,}\S"), "code hidden after a long run of whitespace (pushed off-screen)"), ("GLOBAL_REQUIRE", "high", - re.compile(r"global\s*(\[[^=;]{1,60}\]|\.\w+)\s*=\s*require\b"), + re.compile(r"global\s*(\[[^=;]{1,60}\]|\.\w+)\s*=\s*require\b(?!\s*\()"), "require() exported onto the global object"), ("GLOBAL_MARKER", "high", re.compile(r"""global\s*(\.\s*o|\[\s*['"](!|_V|o|r)['"]\s*\])\s*=\s*['"]"""), @@ -66,6 +66,13 @@ ("EVAL", "low", re.compile(r"\beval\s*\("), "eval() call"), ] +# Text after a whitespace run that looks like hidden code rather than formatting +# (aligned comments/tables in vendor bundles and .d.ts files end in a word or a "|"). +HIDDEN_CODE = re.compile( + r"global\s*[\[.]|require\s*\(|\beval\s*\(|Function\s*\(|fromCharCode|child_process" + r"|_\$_[0-9a-fA-F]{4}|\bimport\s*\(|https?://|\batob\s*\(|Buffer\.from") +HIDDEN_CODE_MIN = 200 # payloads hide thousands of characters after the padding + SUSPICIOUS_CMD = re.compile( r"curl|wget|node\s+-e|base64|powershell|Invoke-|\|\s*(sh|bash)\b|https?://") LIFECYCLE = ("preinstall", "install", "postinstall", "prepare", "prepublish") @@ -180,6 +187,12 @@ def scan_text(path, text, real_path=None): for rid, sev, rx, desc in RULES: m = rx.search(line) if m: + if rid == "WHITESPACE_PADDING" and not is_config: + tail = line[m.end() - 1:].strip() + is_comment = line.lstrip().startswith(("*", "//", "/*", "#")) + if not HIDDEN_CODE.search(tail) and (is_comment or len(tail) < HIDDEN_CODE_MIN): + sev = "low" + desc = "long run of whitespace followed by short text (likely formatting)" if is_config and sev == "medium": sev = "high" findings.append((sev, rid, path, n, desc, preview(line, m.start())))