GCA · LANGUAGE GUIDES

    JavaScript & TypeScript

    Deep-dive: shared scanner across JS and TS, with framework-aware taint tracking.

    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 typeApplies?Depth
    SASTShared JS/TS front-end; per-file N-hop interprocedural taint plus a wired cross-file workspace pass.
    SCAnpm/yarn/pnpm lockfile parsers; OSV, license, health, reachability.
    SecretsLanguage-agnostic.
    ConfigLanguage-agnostic (CI/CD, .env, MCP, system prompts).
    APIOpenAPI/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 tieringprocess.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:

    ClassSink (as it appears in code)CWE
    Code injectioneval(x), new Function(x)CWE-95
    DOM XSSel.innerHTML = x, el.outerHTML = x, document.write(x)CWE-79
    React XSS<div dangerouslySetInnerHTML={{ __html: x }} />CWE-79
    Command injectionchild_process.exec(x), .spawn(x)CWE-78
    SQL injectiondb.query(`SELECT ... ${uid}`)CWE-89
    Prototype pollutionObject.prototype.isAdmin = trueCWE-1321
    ReDoS / dynamic RegExpnew RegExp(userInput)CWE-1333
    Path traversalres.sendFile(req.query.path)CWE-22
    app.post("/run", (req, res) => { // Express registration → entry point
    const code = req.body.code; // known-input source, Remote
    eval(code); // CWE-95 code injection
    });
    el.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_declaration and named function_expression callees are followed; arrow-function and method callees are excluded (methods use a class-this environment 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-taint switch 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 validator if-guard dominates the sink (enclosing if (isValid(p)) { sink }, or early-exit if (!isValid(p)) throw; … sink). JS uniquely includes throw as an early-exit kind. This ships inert today: the shipped sanitizer registry is empty and no JS/TS rule declares dominating_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.

    ParserInputNotes
    package-lock.jsonnpmv2/v3 preferred; v1 best-effort
    yarn.lockYarnClassic v1 (hand-rolled) + Berry v2 (YAML); every entry marked Direct (the lockfile lacks direct/transitive)
    pnpm-lock.yamlpnpmv5/v6; skips link:/file:/tarball/workspace:* entries

    False positives, provenance, and test scope

    • Entry-point narrowing — generic parameter names event, ctx, and context do not promote a function to entry point on a bare-name match (only req/request do). REST-verb name prefixes (get*, post*) require a corroborating request-shaped parameter, so getUserById is not treated as an HTTP handler.
    • Footgun receiver suppression — the bare built-ins eval, exec, Function, compile match only on an exact name or name. prefix, never on a dotted-receiver suffix, so self.model.eval() does not fire.
    • Provenance — tiers flow through the taint origins; a rule with fire: [remote] gates only on Remote-tier flows; process.env/process.argv sources 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_assign matches land at Low confidence and are routed out of the gate; taint-backed ast_flow findings 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) after uid = 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 ns calls, or dynamic import().
    • No browser-DOM sourceslocation, document, window.name are not seeded.
    • Structural guard suppression ships inert (empty registry today).
    • Sink coverage is rule-defined, not engine-defined.

    • 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-security Tier-1 corroboration for JS/TS via --corroborate.