Repository navigation
feat(security): TypeScript/JavaScript sinks for the security lens (M4) - #71
Merged
Merged
Conversation
Milestone 4: the lens covers a second language. `modscan-audit --language
typescript` (or javascript) maps eval, Function/new Function, the vm module, the
child_process family, dynamic require/import, and string-body timers, reusing the
tree-sitter front-end the moddability side already ships.
Modelled on eslint-plugin-security, the JS counterpart to Bandit. Two of its
rules shaped the catalog: a computed require(name) is a sink while a literal
require('fs') is not (detect-non-literal-require), and the child_process family
is split shell vs no-shell the way the Python catalog splits os.system from the
exec family.
Resolves bindings per file before matching. Idiomatic JS writes
`const {exec} = require('child_process')` and then `exec(...)`, so matching only
the qualified form would miss most real code — while matching the bare name would
fire on `someRegex.exec(str)`, which is everywhere and harmless. Each file's
require/import bindings are resolved first, covering the aliased, destructured,
named and namespace forms. This is deliberately file-local: a binding passed
through another module is not followed, which is a stated limit rather than an
oversight.
Verified on a fixture holding every catalogued sink in the forms real code uses,
plus benign look-alikes: JSON.parse, a regex .exec, a literal require, ordinary
fs use, a function-body setTimeout and a property named eval all stay silent.
Python remains the default language, so the released call signature and CLI form
are unchanged. Suite 25/25; benchmark 9/20; the Python Bandit cross-check still
reports 27/27. The test skips cleanly when tree-sitter is absent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
MODScan attack-surface diff —
|
MODScan extension-point diffNo breaking changes to extension points Added (new extension points)
|
This was referenced Jul 23, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Milestone 4 — the security lens covers a second language.
modscan-audit --language typescript(orjavascript) mapseval,Function/new Function, thevmmodule, thechild_processfamily, dynamicrequire/import, and string-body timers, reusing the tree-sitter front-end the moddability side already ships.Closes #62.
Modelled on eslint-plugin-security
The JS counterpart to Bandit. Two of its rules shaped the catalog directly:
detect-non-literal-require— a computedrequire(name)is a sink; a literalrequire('fs')is not. Deliberately stricter than the Python side (whereimport_modulealways fires) because the base rates differ by ecosystem. This validated a design call I'd made before finding the rule.detect-child-process— split shell (exec/execSync, high) vs no-shell (spawn/execFile/fork, medium), the same split the Python catalog makes.Where we go further: eslint-plugin-security has no rule for
new Functionorvm.*. Its out-of-scope rules (unsafe regex, object injection, timing attacks, fs filenames) are deliberately not mirrored — the lens doesn't claim them.Scope addition beyond the plan: binding resolution
The plan assumed qualifier-strict matching would do. It won't. Idiomatic JS writes
const {exec} = require('child_process')thenexec(...)— matching onlychild_process.execmisses most real code, while matching the bare nameexecfires onsomeRegex.exec(str), which is everywhere and harmless.So each file's
require/importbindings are resolved before matching, covering aliased, destructured, named and namespace forms. File-local by design — a binding passed through another module isn't followed. Stated as a limit, not hidden.Verified discrimination
Fixture holds every catalogued sink in the forms real code uses, plus benign look-alikes. All 9 sinks detected at the right lines; zero from the benign half:
eval,new Function,FunctionJSON.parse(thejson.loadsanalogue)vm.runInThisContext(namespace import)re.exec(a regex, not child_process)cp.exec(aliased),exec(destructured)require('fs')(literal → static import)execFile(named import)setTimeout(() => …)(function, not string)require(name)(computed),setTimeout('…')evalHonest limitation
There is no cross-check authority for TS here. Python was validated against Bandit at 27/27; TS validation is planted fixtures + decoys only. I am not claiming TS is validated to the Python standard. Node 24 is available, so an eslint-plugin-security cross-check is feasible as a follow-up — the rule mapping is already worked out above.
Test plan
tests/test_security_ts_sinks.py— catalog integrity, all sinks detected, benign half silent, shell/no-shell severity split,javascriptalias, unknown-languageValueError. Skips cleanly without tree-sitter.import modscanok.--language typescriptrenders a ranked report.🤖 Generated with Claude Code