node720-static · SAST / taint reachability
Then we read every single finding against the actual flagged source. The libraries were clean — so the honest test was our own false-positive rate. It told us where we were wrong, we fixed the root cause, and we turned the whole trial into a standing regression gate.
A SAST scanner only earns trust on code it has never seen. We froze an engine snapshot and ran node720 scan over the latest published source of 22 of the most-depended-on npm packages — roughly 250,000 lines. Then a 12-agent triage pass plus a manual root-cause review read the actual flagged source for every finding and labelled each one true-positive, false-positive, or conditional.
Ten of these packages are so heavily audited that the correct answer is nothing. node720-static returned exactly that — zero findings, zero noise — on all ten:
node720-static keeps two tiers strictly separate: a confirmed tier (Class A/B-high) that is meant to be safe to fail a CI build on, and an informational C tier (reachability hints, never a headline). The flagship promise is zero false positives at the confirmed tier.
On this set, that promise failed. The confirmed tier fired twice — once on pug, once on commander — and both were false positives. Either one would have broken a popular, CVE-free package's build on intended behaviour. The C tier, meanwhile, was ~89% false-positive and found no true bugs. We are showing you the failure because the fix is the product.
| Package | Confirmed | C-tier (info) | Verdict on every finding |
|---|---|---|---|
| pug | 1 | 4 | FP (template compile) + conditional reachability |
| commander | 1 | 0 | FP (spawn(process.argv[0])) |
| moment | 0 | 30 | FP — proto reads + minified-bundle duplicates |
| qs | 0 | 13 | FP — guarded merge + cross-file over-fire |
| cheerio | 0 | 9 | FP — DOM .find() read as a Mongo query |
| ejs · lodash | 0 | 10 | FP — flagged the library's own mitigations |
| handlebars · async · minimist · node-fetch | 0 | 6 | FP + conditional (node-fetch SSRF) |
| TOTAL · 22 pkgs | 2 | 72 | 0 true positives · 0 crashes |
// Tiers are reported separately on purpose. The confirmed tier is the only one sold as 0-FP; C is explicitly noisy reachability and must never blend into the headline number. Both numbers above are pre-fix. §05 is the after.
Both trace to one unsound assumption: that a library's own API parameters are attacker-controlled. Scanned in isolation, a library has no HTTP request, no CLI argv — so seeding taint from its exported parameters manufactures "untrusted" data that does not exist.
This is the compile-and-eval step every template engine performs. The "source" was options.filename / the template — a library API parameter, not a proven untrusted boundary.
Template→Function is the SSTI/RCE class — but only if the application
compiles an untrusted template. You cannot statically confirm that against pug in isolation.
Legitimate as a C-tier "dangerous if the template is untrusted" note. Unjustifiable as a build-failing finding.
The "tainted program" is process.argv[0] — the Node interpreter path. An
attacker controls argv[1..], never argv[0].
No shell:true, so args cannot inject a shell command, and
the spawned program is node itself — the standard mechanism for launching a subcommand.
The tracker folded a taint marker into process.argv and treated
argv[0] as input. It is not. Wrong even in an app context.
The root fix (P0) is a trust-source model: taint originates only at a
genuine untrusted boundary — an HTTP request member, CLI argv[2+],
a file/network read, or an explicit taint declaration. A flow whose only origin is an exported API
parameter may still surface at C (reachability) but can never mint a confirmed verdict. The
param-as-source recall lever stays — it just can't fail your build alone. Five detector fixes clear the C-tier noise.
argv[0]/argv[1] (node + script path) excluded from CLI sources. Fixes both confirmed FPs.target[k]=src[k] copy (handlebars, ejs, qs) is cleared.RegExp.exec, TypedArray.set) on benign first-party helpers.*.min.js, *.bundle.js, dist/, and — added in round two — any file with a generator banner (js_of_ocaml, @generated, webpack) or a minified signature. Generated bundles aren't authored source; they re-report sinks and trip on runtime machinery..find() or an Array.find predicate no longer collides with mongo.query.Object.assign is flagged only when the source is a genuine untrusted boundary — not a dev-supplied config/callback. The fabricated "request body" wording is gone.Re-scanning the locally reproducible subset of the corpus with the fixed engine: the build-failing tier no longer fires on a single audited library, and the C-tier noise collapses. The genuinely-useful conditional detections (pug/template, node-fetch/SSRF, traversal) survive — correctly — as honest C-tier reachability.
// "Reproducible subset" = the corpus packages installed in the test environment (moment and node-fetch were not, so their pre-fix 32 C-tier findings are excluded from the −78% to keep the comparison apples-to-apples). The headline guarantee — 0 confirmed-tier FPs — is measured across the full installed corpus.
Suppressing library-param noise does not blunt the engine on genuinely vulnerable code. Here is a real, non-obvious 2026 CVE node720-static confirms end-to-end — a tainted span that reaches the SQL sink past every value check — alongside an honest conditional trace that is correctly held at C.
req.query.appends{ appends } into .find()The SSRF shape is genuine, but the source is a library parameter, not a proven request boundary. Under the trust-source model this can reach C (reachability) — "SSRF if the app feeds an untrusted URL here" — but it can never become a confirmed, build-failing verdict.
That is the difference the trial taught us: a dangerous shape is a hint; a dangerous shape fed by a proven untrusted source is a finding.
// node720-static is measured at 91/100 on the held-out real-vuln corpus (reachability recall 90.7%, false-positive rate 0/64), and the same detector core runs byte-for-byte at runtime in node720-rasp. See How it works for an end-to-end RCE caught by both engines.
A one-off audit ages out. So the corpus is a re-runnable battery: it hard-fails CI on a single confirmed-tier finding on any audited library, and tracks each package's C-tier count against a recorded ceiling so a detector change cannot quietly regress precision.
// The same discipline as the engine's existing fpRate 0/64 guarantee — now extended to real, audited, third-party code. Stability held throughout: 0 crashes, before and after.
A 0-FP claim is only as good as the next package it has never seen. So we kept going — ten, then twenty, then a hundred more, all at their current versions — to a standing corpus of 150 of the most-depended-on libraries, the dependency backbone of npm:
The confirmed tier stayed at zero the whole way — but each sweep did surface real precision gaps, and that is exactly what it is for. Round two caught xml2js: four confirmed-tier hits, all inside one 3.4 MB, 28,000-line file:
The four hits were eval() calls inside the OCaml-to-JS runtime's own
primitives (caml_js_expr, caml_js_eval_string…) — not
authored application code, and not reachable by any web request.
The readable source (parser.js, builder.js) was clean. The bundle was the problem — so we taught the scanner to skip generated artifacts by content (a generator banner or a minified signature), not just by filename.
Result: 4 → 0. A real precision gap on real code, found by the sweep and closed the same day — then locked behind the standing gate so it can't come back.
Round three's twenty packages produced no confirmed-tier hits, but a wave of C-tier
Object.assign findings mislabelled internal config merges as
mass-assignment. A merge into a fresh object is a clone, not privilege escalation.
So mass-assignment now fires only when the source is genuinely request-derived
(preserving the real Object.assign(entity, req.body) exploit), and cross-file
data-shape reachability is no longer double-counted across module boundaries.
Result: C-tier noise on the corpus cut from 136 to 64 — confirmed tier still zero.
The 100-package sweep's confirmed hits were a dependency's own test suite (intentional
__proto__ payloads it asserts are blocked) and a build bundle — not code
anyone deploys.
So the scanner now skips dependency test/benchmark trees and build-output dirs by default. The dependency backbone of npm, scanned clean.
Staying quiet on clean code is half the job. The other half: does it fire on the vulnerable one? Same library, two versions —
ejs@2.5.7 (the vulnerable version): 1 confirmed code-injection finding. ejs@latest (patched): 0. The same scan, the same rule — it fires on the defect and stays silent on the fix.
Beyond libraries, on a vulnerable application (DVNA) with real request→sink flows the confirmed tier lights up as designed. Audited code stays clean; vulnerable code does not.
// Across four rounds — 150 packages, the dependency backbone of the npm ecosystem — node720-static's confirmed tier fires zero false positives, with zero crashes, while still catching real, documented vulnerabilities in the versions that have them. Every gap each sweep exposed is a regression test today (threat-intel/realworld-corpus.js).
node720-static reuses the runtime detectors byte-for-byte to find source→sink flows — no runtime trigger required. SARIF or text output, guard-aware, inter-procedural and cross-file.