node720-static · SAST / taint reachability

Find the vulnerability. Skip the false alarm.

node720-static traces untrusted input from source to sink across your code — the same detectors that run in production at runtime, no trigger required. Validated on the npm packages you already depend on: it stays quiet on clean code and fires on the real bugs.

150
real npm packages
scanned · four rounds
0
confirmed-tier false
positives · all 150
0
scan crashes
across the corpus
13/13
documented CVEs flagged
in vulnerable versions
01 — HOW TO RUN

One command. SARIF or text.

node720-static reuses the runtime detectors byte-for-byte to find source→sink flows in your code — no runtime trigger required. Point it at a directory or a file:

$ npx node720 scan ./src # human-readable text $ npx node720 scan ./src --format sarif # SARIF → upload to CI / code scanning
taint
Inter-procedural & cross-file
Follows untrusted input through helpers and across require/import boundaries to the sink.
guards
Guard-aware
Escapers, validators, allowlists and path-containment clear a flow — they’re not flagged.
tiers
Trust-source model
A confirmed (build-failing) verdict needs a real untrusted boundary; a library-param flow stays informational reachability.
CI
SARIF / text · IAST bridge
Drop SARIF into code scanning; a runtime trace can upgrade reachable → confirmed.
02 — WHAT WE TESTED & WHAT WE FOUND

No false alarms on clean code. 13/13 real bugs caught.

Two separate tests — precision and recall — kept apart on purpose. For a SAST scanner they are not the same question, and a number that blends them hides the only thing that matters: did it find the real bug, and only the real bug?

A · PRECISION — 150 latest popular packages (audited)

These libraries are heavily audited and patched, so there were no real vulnerabilities to find — and node720 found none. The honest metric here is whether it stays quiet:

0
real vulnerabilities found
— none to find (audited code)
0
confirmed-tier
false positives · all 150
0
scan crashes
across the corpus
150
packages · four rounds
every finding hand-triaged

B · RECALL — 13 packages pinned to known-vulnerable VERSIONS

A different set — the same packages (and others) pinned to the versions that had documented CVEs — to prove it fires on the real bug, and stays silent on the patched build:

13/13
documented CVEs caught
in the vulnerable versions
0
flagged on the
PATCHED versions · no over-fire
5
detector classes fired
code · sql · proto · redos · deser
2
confirmed RCE-grade
ejs code-inj · node-serialize eval

// The two are kept separate on purpose. Blending them into "150 packages, 13 CVEs" would imply the CVEs live in the latest packages — they do not: across the 150 audited packages the real-vulnerability count is 0 (there were none to find), and the 13/13 is a by-VERSION battery. Audited code → 0 false positives; vulnerable code → 13/13 recall (the real bug, not the fix). Held-out real-vuln corpus: 92/100, false-positive rate 0/63; every gap is a re-runnable regression test (threat-intel/realworld-corpus.js).

03 — REAL DETECTIONS & DISCOVERIES

What the detectors actually found.

Two headline traces — a real SQL-injection confirmed end-to-end, and a code-injection caught in the vulnerable version but not the patch — then the full ledger of which detector found which real issue.

@nocobase/database  CVE-2026-41640 · detector SQL · CWE-89
Confirmed · build-failing
// attacker controls an association name; recursive eager- // loading concatenates it straight into SQL. app.get('/api/:table', (req, res) => { const appends = req.query.appends // untrusted source db.repository(req.params.table).find({ appends }) }) // …deep in the ORM, once per association: sql += ` LEFT JOIN ` + assoc + ` ON …` // SQL sink
The trace node720 reports
source · taintedreq.query.appends
propagates · object propertythrough { appends } into .find()
cross-file · ORM internalsrecursive eager-load, once per association name
sink · keyword positiontainted span becomes SQL structure, not a value → injection
ejs  CVE-2017-1000228 · detector code · CWE-94
True positive · vulnerable vs. patched
// [email protected] — the attacker-controllable option is // interpolated into the compiled function's param list: fn = new Function(opts.localsName + ', escapeFn, …', src) // ▲ unsanitized → code injection // ejs@latest — localsName is validated first → // node720 reports nothing. Fires on the defect, not the fix.
It catches the real one — and only the real one

[email protected] (vulnerable): 1 confirmed code-injection. ejs@latest (patched): 0. Same scan, same rule.

On the vulnerable-version battery the code detector also caught node-serialize’s eval RCE; the proto floors caught the lodash/minimist/set-value family; the ReDoS detector caught marked’s catastrophic regex — 13/13.

The full ledger — detector → the real issue it surfaced:

CWE-94
Code-gen / eval
[email protected] unsanitized new Function(opts.localsName…) · node-serialize eval RCE — and the vulnerable liquidjs CLI re-tiered honestly (operator input ≠ deploy boundary).
CVE-2017-1000228
CWE-89
SQL · tokenizer differential
A tainted span that fills a value slot passes; one that adds a keyword/operator/comment is injection — @nocobase association name reaching LEFT JOIN.
CVE-2026-41640
CWE-1321
Prototype-pollution gadget floors
The dotted-path set / recursive-merge family — lodash, minimist, set-value, mixin-deep, defaults-deep, flat — plus the parse-server inherited-dispatch guard.
7 CVEs
CWE-1333
ReDoS · nested quantifiers
Catastrophic-backtracking regexes ((a+)+ / ( …\n*)+) in marked’s inline grammar.
fix
Trust-source model · discovery
It found our own false alarms: pug’s template-compile and commander’s spawn(process.argv[0]) were re-tiered from confirmed to honest reachability — argv[0] is the node binary, not attacker input.
2 → 0 FP
fix
Noise the sweep taught us to skip
Dependency test payloads (object-path’s own __proto__ tests), generated bundles (xml2js’s 3.4 MB js_of_ocaml output), and DOM .find() mistaken for a Mongo query (cheerio) — all now excluded by construction.
precision
RUN IT ON YOUR CODE

Scan every line before it ships.

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.

$ npx node720 scan ./src --format sarif $ npx node720 scan ./src # human-readable text

→ Full install & CI setup    → Watch one detector think