Skip to content

feat(scan): Node.js SAST coverage — ecosystem rules, template engines… - #137

Merged
ralyodio merged 1 commit into
masterfrom
worktree-njsscan-parity
Aug 14, 2026
Merged

feat(scan): Node.js SAST coverage — ecosystem rules, template engines…#137
ralyodio merged 1 commit into
masterfrom
worktree-njsscan-parity

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

…, missing controls

Closes a coverage gap against njsscan, an established Node scanner: of 78 existing rules only ~20 were JavaScript, and template files were not read at all. Adds three pieces, plus a guard fix that was silently suppressing rules.

packages/scan/src/node-rules.ts — 25 Node-ecosystem rules, each of which has to know which package it is looking at before it can claim anything: vm/vm2 execution, function-reconstructing deserialisers, server-side template injection, shelljs, $where and XPath assembly, XML entity resolution, the JWT none algorithm, broken/ECB/legacy ciphers, autoescape and serialize-javascript opt-outs, reflected CORS origins, headless-browser navigation, zip-slip, Electron renderer settings, disabled security headers, buffer bounds and uninitialised allocation, oversized body limits, and returned stack traces.

packages/scan/src/template-rules.ts — a second engine keyed on file extension rather than language, because the extension is what names the template engine and the engine is what decides which syntax skips escaping. Covers Handlebars/ Mustache, Vue, Pug/Jade, EJS/ECT, Dust, Nunjucks/Twig/Jinja and Haml. Runs alongside the code rules, so a .vue file is still checked as JavaScript.

packages/scan/src/controls.ts — reports controls the tree has no evidence of (headers, CSRF, rate limiting, body size limits). Whole-project, so it accumulates across the walk; opt-in behind threatcrush scan --missing-controls because an absence is a weaker claim than a present defect; and silent unless the tree actually serves HTTP.

Guard fix in code-rules.ts. GENERIC_GUARD is case-insensitive, so the "Allow" in Access-Control-Allow-Origin matched its allow-list heuristic. As the guard is tested against an 8-line window, one CORS header line disabled every guardable rule around it — in the end-to-end fixture it suppressed both the none-algorithm finding and a returned stack trace. The failure mode was the dangerous kind: not fewer findings, none, indistinguishable from clean code. Scoped to the header prefix so real allow-lists, including allow-list: keys in scanned config files, keep guarding. Covered by a regression test.

Written clean-room. njsscan is LGPL-3.0 and this package is MIT, so its rule definitions could not be reused even in translation; only the list of vulnerability classes was taken, which is a fact about the ecosystem rather than an expression of it. Every pattern, guard and message is original and written against this engine's confidence model, which njsscan does not share. Provenance and the licence reasoning are recorded in the module headers, and classes needing analysis this engine does not do are listed in KNOWN_GAPS rather than approximated.

Tests: 252 pass in packages/scan (58 new), 2 in apps/cli; both packages typecheck; CLI builds; verified end to end against a fixture app.

…, missing controls

Closes a coverage gap against njsscan, an established Node scanner: of 78
existing rules only ~20 were JavaScript, and template files were not read at
all. Adds three pieces, plus a guard fix that was silently suppressing rules.

packages/scan/src/node-rules.ts — 25 Node-ecosystem rules, each of which has
to know which package it is looking at before it can claim anything: vm/vm2
execution, function-reconstructing deserialisers, server-side template
injection, shelljs, $where and XPath assembly, XML entity resolution, the JWT
`none` algorithm, broken/ECB/legacy ciphers, autoescape and serialize-javascript
opt-outs, reflected CORS origins, headless-browser navigation, zip-slip,
Electron renderer settings, disabled security headers, buffer bounds and
uninitialised allocation, oversized body limits, and returned stack traces.

packages/scan/src/template-rules.ts — a second engine keyed on file extension
rather than language, because the extension is what names the template engine
and the engine is what decides which syntax skips escaping. Covers Handlebars/
Mustache, Vue, Pug/Jade, EJS/ECT, Dust, Nunjucks/Twig/Jinja and Haml. Runs
alongside the code rules, so a .vue file is still checked as JavaScript.

packages/scan/src/controls.ts — reports controls the tree has no evidence of
(headers, CSRF, rate limiting, body size limits). Whole-project, so it
accumulates across the walk; opt-in behind `threatcrush scan --missing-controls`
because an absence is a weaker claim than a present defect; and silent unless
the tree actually serves HTTP.

Guard fix in code-rules.ts. GENERIC_GUARD is case-insensitive, so the "Allow"
in `Access-Control-Allow-Origin` matched its allow-list heuristic. As the guard
is tested against an 8-line window, one CORS header line disabled every
guardable rule around it — in the end-to-end fixture it suppressed both the
`none`-algorithm finding and a returned stack trace. The failure mode was the
dangerous kind: not fewer findings, none, indistinguishable from clean code.
Scoped to the header prefix so real allow-lists, including `allow-list:` keys
in scanned config files, keep guarding. Covered by a regression test.

Written clean-room. njsscan is LGPL-3.0 and this package is MIT, so its rule
definitions could not be reused even in translation; only the list of
vulnerability classes was taken, which is a fact about the ecosystem rather
than an expression of it. Every pattern, guard and message is original and
written against this engine's confidence model, which njsscan does not share.
Provenance and the licence reasoning are recorded in the module headers, and
classes needing analysis this engine does not do are listed in KNOWN_GAPS
rather than approximated.

Tests: 252 pass in packages/scan (58 new), 2 in apps/cli; both packages
typecheck; CLI builds; verified end to end against a fixture app.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ralyodio
ralyodio merged commit 40585ce into master Aug 14, 2026
7 checks passed
@ralyodio
ralyodio deleted the worktree-njsscan-parity branch August 14, 2026 01:50
id: 'js-uninitialized-buffer',
title: 'buffer allocated without zeroing',
consequence:
'`allocUnsafe` and the old `new Buffer(size)` hand back whatever was previously in that heap memory — keys, session tokens, other users’ request bodies. Anything not overwritten before the buffer is sent leaks it.',
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

69 finding(s)

HIGH/CRITICAL: 12 | MEDIUM: 56 | LOW: 1

Severity Rule Location
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
HIGH sh-eval-expansion .githooks/pre-commit:26
HIGH js-electron-node-integration apps/desktop/src/main/index.ts:22
HIGH sh-remote-script-execution apps/web/public/install.sh:272
HIGH sh-remote-script-execution apps/web/public/install.sh:320
HIGH secret-generic-credential modules/spend-guard/config/example.conf.toml:13
HIGH secret-generic-credential modules/spend-guard/README.md:84
HIGH secret-generic-credential PRD.md:268
HIGH tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:121
HIGH tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:122
HIGH sh-remote-script-execution scripts/smoke-test.sh:46
HIGH sh-remote-script-execution scripts/smoke-test.sh:47
MEDIUM insecure-temp-file .githooks/commit-msg:16
MEDIUM insecure-temp-file .githooks/post-commit:20
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:70
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/init.ts:79
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:88
MEDIUM js-shell-exec-interpolation apps/cli/src/commands/service.ts:111
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:121
MEDIUM sql-template-interpolation apps/cli/src/core/state.ts:125
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:31
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:33
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:34
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:35
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:36
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:43
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:49
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:56
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:63
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:82
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:84
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:85
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:93
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:98
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:105
MEDIUM js-shell-exec-interpolation apps/cli/src/daemon/firewall/adapters.ts:112
MEDIUM js-shell-exec-interpolation apps/cli/src/index.ts:425
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:180
MEDIUM js-unescaped-html-sink apps/web/src/app/about/page.tsx:184
MEDIUM js-open-redirect apps/web/src/app/auth/login/page.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:125
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:153
MEDIUM js-unescaped-html-sink apps/web/src/app/blog/[slug]/page.tsx:157
MEDIUM js-unescaped-html-sink apps/web/src/app/get-whitepaper/page.tsx:346
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:211
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:215
MEDIUM js-unescaped-html-sink apps/web/src/app/layout.tsx:219
MEDIUM js-unescaped-html-sink apps/web/src/app/page.tsx:120
MEDIUM js-unescaped-html-sink apps/web/src/app/store/[slug]/page.tsx:107
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97

…and 19 more. Full results in the Security tab.

Snippets are redacted; ThreatCrush never prints matched credential material.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants