JavaScript & TypeScript — Deep-Dive Guide
JavaScript and TypeScript are primary languages and share a single scanner implementation. This guide goes below the summary in languages.md — read that first for the capability matrix, and scan-coverage.md for the six scan types.
One architectural fact frames everything: JS and TS are handled by one
JavaScriptScanner selecting across three tree-sitter grammars — JavaScript
(.js/.mjs/.cjs/.jsx), TypeScript (.ts), and TSX (.tsx). TS files
report Language::TypeScript; both use the same extractor and taint engine.
Which scans apply, and how deep
| Scan type | Applies? | Depth |
|---|---|---|
| SAST | ✅ | Shared JS/TS front-end; per-file N-hop interprocedural taint plus a wired cross-file workspace pass. |
| SCA | ✅ | npm/yarn/pnpm lockfile parsers; OSV, license, health, reachability. |
| Secrets | ✅ | Language-agnostic. |
| Config | ✅ | Language-agnostic (CI/CD, .env, MCP, system prompts). |
| API | ✅ | OpenAPI/GraphQL + source route-handler extraction. |
The taint engine
Sources it recognizes
Three source mechanisms live in taint_js/.
1. Known-input calls (JS_KNOWN_INPUT_CALLS) — substring-matched on an
assignment right-hand side:
req.body, req.query, req.params, req.headers, req.cookies, req.json, req.text, req.formData
request.body … (Express/Fastify)
event.queryStringParameters, event.pathParameters, event.body, event.headers (Lambda)
JSON.parse, url.searchParams.get
process.argv, process.env
ctx.req, context.req, c.req.query, c.req.param, c.req.json, c.req.parseBody (Koa/Hono)
2. Provenance tiering — process.argv* → LocalCli; process.env* →
Env; everything else (including req.body and JSON.parse) → Remote.
3. Entry-point parameters — parameters of a function classified as an entry
point are seeded as Remote. Classification uses decorator + name + annotation
heuristics: NestJS @Get/tsoa @Route decorators; handler name prefixes
(handle, on, do) and exact names (handler, middleware, fetch,
Next.js GET/POST verb exports); Express-style registrations
(app.post(...), router.use(...)); and request-shaped parameter names.
Note (a browser-DOM gap): location, document, and window.name are
not seeded taint sources. DOM-based XSS sources are reachable only via a
rule's own pattern, not the taint engine's source seeding.
Sink families and finding classes
Sinks are not hard-coded in the engine — they are declared in GVL rules and
matched at runtime (ast_call, ast_flow, ast_assign). The extractor shapes
each sink family so a rule can match it:
| Class | Sink (as it appears in code) | CWE |
|---|---|---|
| Code injection | eval(x), new Function(x) | CWE-95 |
| DOM XSS | el.innerHTML = x, el.outerHTML = x, document.write(x) | CWE-79 |
| React XSS | <div dangerouslySetInnerHTML={{ __html: x }} /> | CWE-79 |
| Command injection | child_process.exec(x), .spawn(x) | CWE-78 |
| SQL injection | db.query(`SELECT ... ${uid}`) | CWE-89 |
| Prototype pollution | Object.prototype.isAdmin = true | CWE-1321 |
| ReDoS / dynamic RegExp | new RegExp(userInput) | CWE-1333 |
| Path traversal | res.sendFile(req.query.path) | CWE-22 |
javascriptapp.post("/run", (req, res) => { // Express registration → entry pointconst code = req.body.code; // known-input source, Remoteeval(code); // CWE-95 code injection});
javascriptel.innerHTML = req.query.html; // CWE-79 DOM XSS (unless sanitized, below)
Note: eval'd source is not re-parsed; JSX dangerouslySetInnerHTML is
extracted as an assignment node so a rule can match name + value.
Per-file and cross-file taint
JS/TS is not per-file-only. Both layers exist:
- Per-file — a fixed-point analyzer per function, plus N-hop intra-file
interprocedural tracking across same-file callees (bounded). Only
function_declarationand namedfunction_expressioncallees are followed; arrow-function and method callees are excluded (methods use a class-thisenvironment instead). - Cross-file (workspace) — a wired workspace pass (matching the "✅
workspace" row in languages.md) resolves ES6 and CommonJS
imports/re-exports and propagates taint across files under the same
--no-workspace-taintswitch as the other workspace languages. JS and TS files share one workspace.
Honest cross-file limits: resolution follows named function and re-export
chains, but arrow-function callees, namespace import * as ns qualified
calls (ns.func()), and dynamic import() are not followed. import type
is filtered (no runtime taint). Cross-language (e.g. Python↔JS) is out of scope.
Sanitizer / guard recognition
- Structural dominance guards (
taint_js/guard.rs) — suppress only on positive proof that a validatorif-guard dominates the sink (enclosingif (isValid(p)) { sink }, or early-exitif (!isValid(p)) throw; … sink). JS uniquely includesthrowas an early-exit kind. This ships inert today: the shipped sanitizer registry is empty and no JS/TS rule declaresdominating_guards:yet — a documented, tested seam that fails open (finding fires). - Matcher-side sanitizer gates — driven by each rule's
sanitizer_functions:list.el.innerHTML = DOMPurify.sanitize(req.query.html)is suppressed (whole RHS is a direct sanitizer call); a template literal is suppressed only when every${…}interpolation is sanitized; any raw interpolation still fires. Name heuristics (sanitize_*,escape_*,encodeURIComponent) also downgrade.
Excluded files: .min.js, dist, bundles
Minified and generated JS is a large historical FP source, so it is excluded by
both filename globs and a content backstop (skip_paths.rs):
- Minified / hashed bundles —
**/*.min.js,**/*.min.css, and content-hash names like**/*-????????.js(Vite/Rollup/webpack output). - Build output —
**/dist/**,**/.next/**,**/build/**. - Vendor / deps —
**/node_modules/**,**/vendor/**,**/third_party/**, and specific vendored files (**/jquery*.js,**/html5shiv.js, …). - Tests —
**/*.spec.{ts,js},**/*.test.{ts,js},**/__tests__/**,**/cypress/**,**/playwright/**,**/e2e/**. - TS declarations —
**/*.d.ts(ambient types, zero runtime). - Frontend asset trees —
**/public/js/**,**/static/js/**, etc.
A content backstop additionally skips any file whose average line length exceeds ~1000 characters, catching minified bundles that slip past the filename globs.
SCA — npm / yarn / pnpm
All parsers emit Ecosystem::Npm and feed OSV, license, health, and
reachability analysis. These parsers are independent of the SAST skip that
excludes the same lockfiles from code scanning.
| Parser | Input | Notes |
|---|---|---|
package-lock.json | npm | v2/v3 preferred; v1 best-effort |
yarn.lock | Yarn | Classic v1 (hand-rolled) + Berry v2 (YAML); every entry marked Direct (the lockfile lacks direct/transitive) |
pnpm-lock.yaml | pnpm | v5/v6; skips link:/file:/tarball/workspace:* entries |
False positives, provenance, and test scope
- Entry-point narrowing — generic parameter names
event,ctx, andcontextdo not promote a function to entry point on a bare-name match (onlyreq/requestdo). REST-verb name prefixes (get*,post*) require a corroborating request-shaped parameter, sogetUserByIdis not treated as an HTTP handler. - Footgun receiver suppression — the bare built-ins
eval,exec,Function,compilematch only on an exact name orname.prefix, never on a dotted-receiver suffix, soself.model.eval()does not fire. - Provenance — tiers flow through the taint origins; a rule with
fire: [remote]gates only onRemote-tier flows;process.env/process.argvsources demote (see false-positives.md §3). - Test scope — path-based (test globs above), plus a one-band severity demote on non-production paths and pruning of low findings on throwaway test paths.
- Low-confidence SAST is advisory — bare
ast_call/ast_assignmatches land at Low confidence and are routed out of the gate; taint-backedast_flowfindings are Medium and gate.
Honest scope limits
- Call metadata is Phase-1 stubbed for JS/TS — argument counts, keyword arguments, and argument values are not populated (only positional argument shapes are). Rules relying on argument counts or keyword args do not work on JS/TS.
- Flow-insensitive — a re-clean (
uid = sanitize(uid)afteruid = req.body.uid) does not remove taint; relies on matcher sanitizer recognition. - Interprocedural bounds — intra-file N-hop for named functions only; arrow
and method callees excluded. Cross-file follows named/import/re-export chains
but not arrow callees,
import * as nscalls, or dynamicimport(). - No browser-DOM sources —
location,document,window.nameare not seeded. - Structural guard suppression ships inert (empty registry today).
- Sink coverage is rule-defined, not engine-defined.
Related documentation
- languages.md — the cross-language capability matrix.
- scan-coverage.md — the six scan types and eight pillars.
- false-positives.md — confidence, effective risk, provenance, waivers.
- tiers.md — Tier-0/1/2, and
eslint-securityTier-1 corroboration for JS/TS via--corroborate.
