@clear-capabilities/agentic-security-scanner 0.136.9 → 0.137.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +659 -0
- package/bin/agentic-security.js +3 -0
- package/dist/435.index.js +9 -1
- package/dist/agentic-security.mjs +3 -3
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +8 -8
- package/src/dataflow/CLAUDE.md +3 -1
- package/src/dataflow/catalog-expanded.js +1 -0
- package/src/dataflow/catalog.js +157 -31
- package/src/dataflow/engine.js +318 -55
- package/src/dataflow/index.js +15 -0
- package/src/dataflow/points-to.js +19 -6
- package/src/engine.js +281 -207
- package/src/ir/CLAUDE.md +14 -3
- package/src/ir/class-hierarchy.js +57 -11
- package/src/ir/index.js +14 -2
- package/src/ir/parser-cs.js +451 -31
- package/src/ir/parser-java.js +205 -2
- package/src/ir/parser-js.js +149 -2
- package/src/ir/parser-kt.js +436 -18
- package/src/ir/parser-php.js +587 -29
- package/src/ir/parser-py.helper.py +32 -2
- package/src/ir/parser-py.js +31 -4
- package/src/ir/parser-rb.js +124 -19
- package/src/lsp/server.js +7 -1
- package/src/mcp/tools.js +9 -1
- package/src/posture/clustering.js +12 -1
- package/src/posture/sbom.js +2 -2
package/src/ir/parser-java.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
// when the shape is unfamiliar.
|
|
23
23
|
|
|
24
24
|
import { blankComments } from '../sast/_comment-strip.js';
|
|
25
|
+
import { callSitesFromCfg } from './call-sites.js';
|
|
25
26
|
|
|
26
27
|
let _nodeIdSeq = 0;
|
|
27
28
|
function nextNodeId() { return 'jn' + (++_nodeIdSeq); }
|
|
@@ -288,6 +289,160 @@ function buildCfgFromBody(bodyNode) {
|
|
|
288
289
|
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line: _lineOf(w), succ: [] });
|
|
289
290
|
for (const sub of (w.children?.statement || [])) walkStmts(sub);
|
|
290
291
|
}
|
|
292
|
+
if (kids.forStatement) {
|
|
293
|
+
const f = kids.forStatement[0];
|
|
294
|
+
// Both basicForStatement (`for(init;test;step)`) and
|
|
295
|
+
// enhancedForStatement (`for(T x : xs)`) wrap a `statement` child for
|
|
296
|
+
// the loop body — same shape whileStatement already walks. Confirmed
|
|
297
|
+
// via direct java-parser CST inspection (java-parser@3.0.1): each
|
|
298
|
+
// forStatement's `children` has exactly one of these two keys, never
|
|
299
|
+
// both, and each carries `statement` directly.
|
|
300
|
+
const basic = f.children?.basicForStatement?.[0];
|
|
301
|
+
const enhanced = f.children?.enhancedForStatement?.[0];
|
|
302
|
+
const inner = basic || enhanced;
|
|
303
|
+
const cond = basic?.children?.expression?.[0];
|
|
304
|
+
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line: _lineOf(f), succ: [] });
|
|
305
|
+
// R8 Task 1 fix round 1: `for (T x : xs)` never bound `x` to `xs` —
|
|
306
|
+
// the loop body was reachable (the fix above) but the loop variable
|
|
307
|
+
// itself carried no taint provenance, so a genuinely tainted
|
|
308
|
+
// collection reaching a sink through the loop variable still
|
|
309
|
+
// couldn't fire. Confirmed via direct CST inspection:
|
|
310
|
+
// enhancedForStatement.children = {..., localVariableDeclaration,
|
|
311
|
+
// Colon, expression, ..., statement} — `localVariableDeclaration` is
|
|
312
|
+
// the loop var's declaration (same variableDeclaratorList shape used
|
|
313
|
+
// elsewhere in this file) and `expression` is the iterated
|
|
314
|
+
// collection. Mirrors the established pattern in parser-js.js's
|
|
315
|
+
// ForOfStatement handling (PRD R13(b)): synthesize
|
|
316
|
+
// `{kind:'assign', target: loopVar, source: iterExpr}` BEFORE
|
|
317
|
+
// recursing into the body, so any body statement reading the loop
|
|
318
|
+
// variable sees its taint provenance already established. Java's
|
|
319
|
+
// for-each always declares a fresh block-scoped variable (there is
|
|
320
|
+
// no bare-assignment form the way JS's `for (x of xs)` has), so —
|
|
321
|
+
// unlike parser-js.js — there is no pre-existing outer variable of
|
|
322
|
+
// the same name to protect with a post-loop kill; the Java CFG
|
|
323
|
+
// builder does not model block scoping elsewhere either, so adding
|
|
324
|
+
// one only for this binding would be new, inconsistent behavior
|
|
325
|
+
// rather than a fix for this gap.
|
|
326
|
+
if (enhanced) {
|
|
327
|
+
const lvd = enhanced.children?.localVariableDeclaration?.[0];
|
|
328
|
+
const declarator = lvd?.children?.variableDeclaratorList?.[0]?.children?.variableDeclarator?.[0];
|
|
329
|
+
const loopVar = declarator?.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
330
|
+
const iterExpr = enhanced.children?.expression?.[0];
|
|
331
|
+
if (loopVar) {
|
|
332
|
+
emit({ kind: 'assign', target: loopVar, source: iterExpr ? exprFromCst(iterExpr) : { kind: 'unknown' }, line: _lineOf(enhanced), succ: [] });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const sub of (inner?.children?.statement || [])) walkStmts(sub);
|
|
336
|
+
}
|
|
337
|
+
if (kids.doStatement) {
|
|
338
|
+
const d = kids.doStatement[0];
|
|
339
|
+
const cond = d.children?.expression?.[0];
|
|
340
|
+
emit({ kind: 'loop-header', cond: cond ? exprFromCst(cond) : null, line: _lineOf(d), succ: [] });
|
|
341
|
+
for (const sub of (d.children?.statement || [])) walkStmts(sub);
|
|
342
|
+
}
|
|
343
|
+
if (kids.tryStatement) {
|
|
344
|
+
const t = kids.tryStatement[0];
|
|
345
|
+
emit({ kind: 'noop', line: _lineOf(t), succ: [] });
|
|
346
|
+
// Plain `try { ... }` has direct `block`/`catches`/`finally` children.
|
|
347
|
+
// Try-with-resources (`try (Resource r = ...) { ... }` — the idiomatic
|
|
348
|
+
// JDBC shape) wraps in a distinct `tryWithResourcesStatement`
|
|
349
|
+
// intermediate node, and — confirmed via direct CST inspection, this
|
|
350
|
+
// is the one place the plan's illustrative code was wrong — that
|
|
351
|
+
// intermediate node carries its OWN `block`/`catches`/`finally`
|
|
352
|
+
// children; a plain tryStatement's outer `children` is just
|
|
353
|
+
// `{ tryWithResourcesStatement: [...] }` with nothing else, so
|
|
354
|
+
// reading `catches`/`finally` off the outer `t` (as the plan's draft
|
|
355
|
+
// did) silently drops the catch/finally bodies of every
|
|
356
|
+
// try-with-resources block. `container` below picks the node that
|
|
357
|
+
// actually owns them.
|
|
358
|
+
const twr = t.children?.tryWithResourcesStatement?.[0];
|
|
359
|
+
const container = twr || t;
|
|
360
|
+
if (twr) {
|
|
361
|
+
const resSpec = twr.children?.resourceSpecification?.[0];
|
|
362
|
+
// resourceSpecification -> resourceList -> resource[] -> each a
|
|
363
|
+
// localVariableDeclaration-shaped resource; reuse the existing
|
|
364
|
+
// localVariableDeclarationStatement lowering shape directly rather
|
|
365
|
+
// than duplicating it, by walking each resource as if it were one.
|
|
366
|
+
const resources = resSpec?.children?.resourceList?.[0]?.children?.resource || [];
|
|
367
|
+
for (const r of resources) {
|
|
368
|
+
const vdecl = r.children?.localVariableDeclaration?.[0] || r;
|
|
369
|
+
const declarators = vdecl?.children?.variableDeclaratorList?.[0]?.children?.variableDeclarator || [];
|
|
370
|
+
for (const d of declarators) {
|
|
371
|
+
const target = d.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image;
|
|
372
|
+
const initExpr = d.children?.variableInitializer?.[0]?.children?.expression?.[0] || d.children?.expression?.[0];
|
|
373
|
+
if (target) emit({ kind: 'assign', target, source: initExpr ? exprFromCst(initExpr) : { kind: 'unknown' }, line: _lineOf(r), succ: [] });
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const bodyBlock = container.children?.block?.[0];
|
|
378
|
+
if (bodyBlock) walkStmts(bodyBlock);
|
|
379
|
+
const catches = container.children?.catches?.[0]?.children?.catchClause || [];
|
|
380
|
+
for (const cc of catches) {
|
|
381
|
+
const cblock = cc.children?.block?.[0];
|
|
382
|
+
if (cblock) walkStmts(cblock);
|
|
383
|
+
}
|
|
384
|
+
// Confirmed via direct CST inspection: the key is `finally`, no
|
|
385
|
+
// trailing underscore, in both the plain and try-with-resources
|
|
386
|
+
// shapes (java-parser@3.0.1).
|
|
387
|
+
const fin = container.children?.finally?.[0];
|
|
388
|
+
const finBlock = fin?.children?.block?.[0];
|
|
389
|
+
if (finBlock) walkStmts(finBlock);
|
|
390
|
+
}
|
|
391
|
+
if (kids.switchStatement) {
|
|
392
|
+
const sw = kids.switchStatement[0];
|
|
393
|
+
const cond = sw.children?.expression?.[0];
|
|
394
|
+
emit({ kind: 'if', cond: cond ? exprFromCst(cond) : null, line: _lineOf(sw), succ: [] });
|
|
395
|
+
const swBlock = sw.children?.switchBlock?.[0];
|
|
396
|
+
// Classic colon-form (`case 1: ...; break;`).
|
|
397
|
+
const groups = swBlock?.children?.switchBlockStatementGroup || [];
|
|
398
|
+
for (const g of groups) {
|
|
399
|
+
const bss = g.children?.blockStatements?.[0];
|
|
400
|
+
if (bss) walkStmts(bss);
|
|
401
|
+
}
|
|
402
|
+
// R8 Task 1 fix round 1: arrow-form (`case 1 -> ...;`, Java 14+) is a
|
|
403
|
+
// structurally distinct grammar rule — confirmed via direct CST
|
|
404
|
+
// inspection (java-parser@3.0.1) — not an alternate reading of
|
|
405
|
+
// switchBlockStatementGroup. A switchBlock's children carry EITHER
|
|
406
|
+
// switchBlockStatementGroup (colon-form) OR switchRule (arrow-form),
|
|
407
|
+
// never both (Java's grammar forbids mixing them in one switch
|
|
408
|
+
// block), so this is a separate, independent branch rather than a
|
|
409
|
+
// fallback path. Each switchRule carries exactly one body-shape key:
|
|
410
|
+
// `expression` (`case 1 -> sink(x);`), `block`
|
|
411
|
+
// (`case 1 -> { ...; }`), or `throwStatement`
|
|
412
|
+
// (`case 1 -> throw new X();`). Before this fix arrow-form switches
|
|
413
|
+
// fell through this whole branch producing zero walked statements —
|
|
414
|
+
// confirmed 0 IR-TAINT findings for the idiomatic modern-Java shape.
|
|
415
|
+
const rules = swBlock?.children?.switchRule || [];
|
|
416
|
+
for (const r of rules) {
|
|
417
|
+
const rblock = r.children?.block?.[0];
|
|
418
|
+
if (rblock) { walkStmts(rblock); continue; }
|
|
419
|
+
const rthrow = r.children?.throwStatement?.[0];
|
|
420
|
+
if (rthrow) {
|
|
421
|
+
const texpr = rthrow.children?.expression?.[0];
|
|
422
|
+
emit({ kind: 'throw', value: texpr ? exprFromCst(texpr) : null, line: _lineOf(rthrow), succ: [] });
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
const rexpr = r.children?.expression?.[0];
|
|
426
|
+
if (rexpr) {
|
|
427
|
+
const expr = exprFromCst(rexpr);
|
|
428
|
+
if (expr.kind === 'call') emit({ ...expr, line: _lineOf(r), succ: [] });
|
|
429
|
+
else if (expr.kind === 'binary' && expr.op === '=') {
|
|
430
|
+
emit({ kind: 'assign', target: expr.left?.name || null, source: expr.right, line: _lineOf(r), succ: [] });
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// Bare nested block `{ ... }` with no leading keyword. The
|
|
436
|
+
// `statementWithoutTrailingSubstatement` branch above already recurses
|
|
437
|
+
// into each such node via walkStmts; confirmed via direct CST
|
|
438
|
+
// inspection that a bare block lands as
|
|
439
|
+
// `statementWithoutTrailingSubstatement[0].children.block[0]`, so this
|
|
440
|
+
// branch — which fires for any node with a `block` child, not just
|
|
441
|
+
// this one — picks it up on the recursive call without a dedicated
|
|
442
|
+
// bare-block branch.
|
|
443
|
+
if (kids.block) {
|
|
444
|
+
for (const b of kids.block) walkStmts(b);
|
|
445
|
+
}
|
|
291
446
|
}
|
|
292
447
|
|
|
293
448
|
if (nodes[prev]) {
|
|
@@ -335,10 +490,56 @@ export async function parseJavaFile(file, raw) {
|
|
|
335
490
|
const name = md.children?.methodHeader?.[0]?.children?.methodDeclarator?.[0]?.children?.Identifier?.[0]?.image
|
|
336
491
|
|| md.children?.methodDeclarator?.[0]?.children?.Identifier?.[0]?.image
|
|
337
492
|
|| 'anonymous';
|
|
338
|
-
|
|
493
|
+
// Parameter list + annotations live in the same CST subtree, so
|
|
494
|
+
// both are extracted in one walk (PRD R14(a) Task 5). Mirrors the
|
|
495
|
+
// dual-path fallback pattern used above for `name`: depending on
|
|
496
|
+
// which `k` matched in the outer loop, the params live nested
|
|
497
|
+
// under a `methodHeader` node OR directly under `methodDeclarator`.
|
|
498
|
+
const fpl = md.children?.methodHeader?.[0]?.children?.methodDeclarator?.[0]?.children?.formalParameterList?.[0]
|
|
499
|
+
|| md.children?.methodDeclarator?.[0]?.children?.formalParameterList?.[0];
|
|
500
|
+
const paramAnnotations = [];
|
|
501
|
+
const params = (fpl?.children?.formalParameter || []).map((fp, idx) => {
|
|
502
|
+
// Regular parameters nest under `variableParaRegularParameter`.
|
|
503
|
+
// Varargs (`String... args`) instead use a distinct
|
|
504
|
+
// `variableArityParameter` node that this walk doesn't extract
|
|
505
|
+
// from — `vp` is undefined, so this degrades gracefully to a
|
|
506
|
+
// dropped (not corrupted/crashed) parameter, matching the
|
|
507
|
+
// codebase's existing "degrade gracefully rather than throw"
|
|
508
|
+
// convention for shapes that aren't cleanly extractable. Array
|
|
509
|
+
// types (`String[] args`) are unaffected — the `[]` lives in
|
|
510
|
+
// `unannType`, not the identifier, so `variableDeclaratorId`
|
|
511
|
+
// still yields a clean name.
|
|
512
|
+
const vp = fp.children?.variableParaRegularParameter?.[0];
|
|
513
|
+
const paramName = vp?.children?.variableDeclaratorId?.[0]?.children?.Identifier?.[0]?.image || null;
|
|
514
|
+
// Java allows multiple stacked annotations on one parameter
|
|
515
|
+
// (`@NotNull @RequestParam String q`), each its own
|
|
516
|
+
// `variableModifier` entry — loop over all of them, not just
|
|
517
|
+
// the first, or a stacked annotation is silently dropped (the
|
|
518
|
+
// same class of bug Task 3's C# regex needed a fix round for).
|
|
519
|
+
const variableModifiers = vp?.children?.variableModifier || [];
|
|
520
|
+
for (const vm of variableModifiers) {
|
|
521
|
+
const ann = vm.children?.annotation?.[0];
|
|
522
|
+
// `typeName.children.Identifier` is an array of ALL
|
|
523
|
+
// dot-separated segments for a fully-qualified annotation
|
|
524
|
+
// (`@org.springframework.web.bind.annotation.RequestParam`
|
|
525
|
+
// yields ['org','springframework','web','bind','annotation',
|
|
526
|
+
// 'RequestParam']) — the simple annotation name is the LAST
|
|
527
|
+
// segment, not the first. Taking [0] previously recorded the
|
|
528
|
+
// package root ("org") as the decorator name, a silent wrong
|
|
529
|
+
// value rather than the honest drop the C#/JS sibling parsers
|
|
530
|
+
// produce for the same shape (fix round 1, R14(a) Task 5).
|
|
531
|
+
const identifiers = ann?.children?.typeName?.[0]?.children?.Identifier;
|
|
532
|
+
const decoratorName = identifiers?.length ? identifiers[identifiers.length - 1]?.image : undefined;
|
|
533
|
+
if (decoratorName && paramName) {
|
|
534
|
+
paramAnnotations.push({ index: idx, name: paramName, decorator: decoratorName });
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return paramName;
|
|
538
|
+
}).filter(Boolean);
|
|
339
539
|
const body = md.children?.methodBody?.[0]?.children?.block?.[0];
|
|
340
540
|
if (body) {
|
|
341
541
|
const methodLine = _lineOf(md);
|
|
542
|
+
const cfg = buildCfgFromBody(body);
|
|
342
543
|
functions.push({
|
|
343
544
|
// Sibling frontends (parser-js.js) suffix the qid with `@line`
|
|
344
545
|
// so overloaded/same-named methods don't collide — without it,
|
|
@@ -348,8 +549,10 @@ export async function parseJavaFile(file, raw) {
|
|
|
348
549
|
name: className ? `${className}.${name}` : name,
|
|
349
550
|
line: methodLine,
|
|
350
551
|
params,
|
|
351
|
-
cfg
|
|
552
|
+
cfg,
|
|
352
553
|
file,
|
|
554
|
+
calls: callSitesFromCfg(cfg),
|
|
555
|
+
...(paramAnnotations.length ? { paramAnnotations } : {}),
|
|
353
556
|
});
|
|
354
557
|
}
|
|
355
558
|
}
|
package/src/ir/parser-js.js
CHANGED
|
@@ -63,11 +63,23 @@ function exprOf(n) {
|
|
|
63
63
|
prop: n.computed ? (n.property?.value != null ? String(n.property.value) : '*') : (n.property?.name || '*'),
|
|
64
64
|
};
|
|
65
65
|
case 'CallExpression':
|
|
66
|
-
case 'OptionalCallExpression':
|
|
66
|
+
case 'OptionalCallExpression': return {
|
|
67
|
+
kind: 'call',
|
|
68
|
+
callee: exprOf(n.callee),
|
|
69
|
+
args: (n.arguments || []).map(exprOf),
|
|
70
|
+
};
|
|
71
|
+
// `new Foo()` is emitted as a call PLUS an `isNew: true` marker, matching
|
|
72
|
+
// what parser-java.js and parser-cs.js already emit. Without the marker a
|
|
73
|
+
// `new Foo()` is byte-identical in the IR to a plain `Foo()` call, and
|
|
74
|
+
// class-hierarchy.js's typeOfVar walker therefore typed `const x =
|
|
75
|
+
// SomeFactoryFn()` as class `SomeFactoryFn` — a fabricated type that then
|
|
76
|
+
// reaches the dataflow receiver-type gate and can suppress a real finding.
|
|
77
|
+
// Nothing else in the engine reads `isNew`, so this is purely additive.
|
|
67
78
|
case 'NewExpression': return {
|
|
68
79
|
kind: 'call',
|
|
69
80
|
callee: exprOf(n.callee),
|
|
70
81
|
args: (n.arguments || []).map(exprOf),
|
|
82
|
+
isNew: true,
|
|
71
83
|
};
|
|
72
84
|
case 'BinaryExpression': return { kind: 'binary', op: n.operator, left: exprOf(n.left), right: exprOf(n.right) };
|
|
73
85
|
case 'LogicalExpression': return { kind: 'logical', op: n.operator, left: exprOf(n.left), right: exprOf(n.right) };
|
|
@@ -151,6 +163,7 @@ export function parseJsFile(file, code) {
|
|
|
151
163
|
const qid = fnQid(file, scopeName, name, line);
|
|
152
164
|
const entryId = nextNodeId();
|
|
153
165
|
const exitId = nextNodeId();
|
|
166
|
+
const paramAnnotations = [];
|
|
154
167
|
const fn = {
|
|
155
168
|
qid, name: name || 'anon', line,
|
|
156
169
|
// Plain strings, per the IR shape contract (ir/CLAUDE.md: "params:
|
|
@@ -163,8 +176,34 @@ export function parseJsFile(file, code) {
|
|
|
163
176
|
// mutated-parameter taint and context-sensitive entry states were both
|
|
164
177
|
// unconditionally inert for every JS/TS function. Nothing in src/
|
|
165
178
|
// reads a param's .kind or .props, so the richer shape bought nothing.
|
|
166
|
-
params: (params || []).map(p => {
|
|
179
|
+
params: (params || []).map((p, idx) => {
|
|
167
180
|
if (!p) return null;
|
|
181
|
+
// NestJS/Angular-style parameter decorators (@Query(), @Body(), etc.)
|
|
182
|
+
// — Babel attaches these to the raw param node as `p.decorators`
|
|
183
|
+
// (or `p.left.decorators` for a defaulted param, e.g. `@Query() page
|
|
184
|
+
// = 1` — a very common NestJS idiom). This is a real array; every
|
|
185
|
+
// entry must be captured, not just the first, since stacked
|
|
186
|
+
// decorators (@Query() @SomeOtherDecorator() x) are legal and
|
|
187
|
+
// dropping later ones silently loses the source-relevant one.
|
|
188
|
+
//
|
|
189
|
+
// For a defaulted param, `p.type` is 'AssignmentPattern', never
|
|
190
|
+
// 'Identifier' — the identifier that actually carries the decorator
|
|
191
|
+
// and the name lives at `p.left`. The guard below must check THAT
|
|
192
|
+
// resolved node's type, not `p.type` unconditionally, or every
|
|
193
|
+
// defaulted-identifier decorated parameter silently loses its
|
|
194
|
+
// decorator (fix round 1, R14(a) Task 4).
|
|
195
|
+
const resolvedIdent = p.type === 'AssignmentPattern' ? p.left : p;
|
|
196
|
+
const decoratorNodes = p.decorators || (p.left && p.left.decorators) || [];
|
|
197
|
+
for (const d of decoratorNodes) {
|
|
198
|
+
const expr = d.expression;
|
|
199
|
+
const decoratorName = expr?.type === 'CallExpression' ? expr.callee?.name : expr?.name;
|
|
200
|
+
// Only record when the parameter itself resolves to a plain
|
|
201
|
+
// identifier — decorators on destructured params are rare and
|
|
202
|
+
// out of scope for this plan.
|
|
203
|
+
if (decoratorName && resolvedIdent?.type === 'Identifier') {
|
|
204
|
+
paramAnnotations.push({ index: idx, name: resolvedIdent.name, decorator: decoratorName });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
168
207
|
if (p.type === 'Identifier') return p.name;
|
|
169
208
|
if (p.type === 'ObjectPattern') return '<obj>';
|
|
170
209
|
if (p.type === 'ArrayPattern') return '<arr>';
|
|
@@ -181,6 +220,7 @@ export function parseJsFile(file, code) {
|
|
|
181
220
|
writes: new Map(),
|
|
182
221
|
file,
|
|
183
222
|
_cursor: entryId, // current node ID — next addNode() links from here
|
|
223
|
+
...(paramAnnotations.length ? { paramAnnotations } : {}),
|
|
184
224
|
};
|
|
185
225
|
fn.cfg.nodes.set(entryId, { id: entryId, kind: 'entry', succ: [], pred: [], line });
|
|
186
226
|
fn.cfg.nodes.set(exitId, { id: exitId, kind: 'exit', succ: [], pred: [], line });
|
|
@@ -319,6 +359,35 @@ export function parseJsFile(file, code) {
|
|
|
319
359
|
|
|
320
360
|
VariableDeclarator(path) {
|
|
321
361
|
const fn = currentFn(); if (!fn) return;
|
|
362
|
+
// PRD R13(b): `for (const item of tainted)` is, structurally, also
|
|
363
|
+
// an ordinary VariableDeclarator (`item`, no `init`) — Babel visits
|
|
364
|
+
// it as a normal child of the ForOfStatement's `left` on the way
|
|
365
|
+
// into the loop body. Left to the general case below, this generic
|
|
366
|
+
// visit fires AFTER the loop visitor's enter() hook synthesizes
|
|
367
|
+
// `item = <iterated expr>`, re-assigning `item` from an absent
|
|
368
|
+
// `init` (source: unknown) and silently erasing the taint just
|
|
369
|
+
// synthesized. Skip it here ONLY for the simple-identifier shape
|
|
370
|
+
// the loop visitor actually owns and synthesizes for — a bare
|
|
371
|
+
// `for (const item of ...)` binding.
|
|
372
|
+
//
|
|
373
|
+
// A destructuring for-of binding (`for (const {a,b} of ...)` /
|
|
374
|
+
// `for (const [a,b] of ...)`) is explicitly NOT covered by the
|
|
375
|
+
// loop visitor's synthesis (see the ForOfStatement branch below —
|
|
376
|
+
// `loopVar` stays null and nothing is emitted for it), so it MUST
|
|
377
|
+
// fall through to the general destructuring handling further down
|
|
378
|
+
// in this same visitor. That handling was already here before this
|
|
379
|
+
// task and does something this guard must not break: it emits real
|
|
380
|
+
// taint-KILL assign nodes for each destructured name, which is what
|
|
381
|
+
// makes `let cmd = tainted; for (const {cmd} of SAFE) sink(cmd)`
|
|
382
|
+
// correctly clear cmd's stale outer taint. An earlier version of
|
|
383
|
+
// this guard matched on the ForOfStatement `left` alone (no
|
|
384
|
+
// Identifier check) and silently deleted those taint-kill nodes,
|
|
385
|
+
// producing a false positive on exactly that shadowing shape — see
|
|
386
|
+
// the regression test below.
|
|
387
|
+
const gpDecl = path.parentPath && path.parentPath.node;
|
|
388
|
+
const gpLoop = path.parentPath && path.parentPath.parentPath && path.parentPath.parentPath.node;
|
|
389
|
+
if (gpLoop && gpLoop.type === 'ForOfStatement' && gpLoop.left === gpDecl
|
|
390
|
+
&& path.node.id?.type === 'Identifier') return;
|
|
322
391
|
const id = lhsPath(path.node.id);
|
|
323
392
|
if (!id) return;
|
|
324
393
|
const initExpr = exprOf(path.node.init);
|
|
@@ -492,6 +561,64 @@ export function parseJsFile(file, code) {
|
|
|
492
561
|
fn.cfg.nodes.set(exitId, { id: exitId, kind: 'noop', succ: [], pred: [], line });
|
|
493
562
|
path.node._loopHeader = headerId;
|
|
494
563
|
path.node._loopExit = exitId;
|
|
564
|
+
// PRD R13(b): for-of's binding variable is never connected to the
|
|
565
|
+
// iterated expression, so `for (const x of tainted) sink(x)` reads
|
|
566
|
+
// x as {kind:'unknown'} — clean. Synthesize an assign binding the
|
|
567
|
+
// loop variable to the iterated expression, exactly as the
|
|
568
|
+
// Python-CST/Go/PHP parsers already do for their own for-each
|
|
569
|
+
// constructs (parser-py.helper.py, parser-go.js, parser-php.js).
|
|
570
|
+
// Conservative, matching this file's own stated doctrine for
|
|
571
|
+
// loops in general ("any iteration could taint X"): the WHOLE
|
|
572
|
+
// loop variable is tainted if the iterable is tainted, not a
|
|
573
|
+
// specific element — element-level precision isn't modeled here
|
|
574
|
+
// any more than it is for the rest of this file's loop handling.
|
|
575
|
+
// Scoped to ForOfStatement only — While/For/DoWhile/ForIn have no
|
|
576
|
+
// "loop variable bound to an iterated collection" shape and must
|
|
577
|
+
// see zero behavior change from this addition.
|
|
578
|
+
if (path.node.type === 'ForOfStatement') {
|
|
579
|
+
const leftNode = path.node.left;
|
|
580
|
+
const declId = leftNode && leftNode.type === 'VariableDeclaration'
|
|
581
|
+
? leftNode.declarations[0]?.id
|
|
582
|
+
: leftNode;
|
|
583
|
+
// Only the simple `for (const x of ...)` / `for (x of ...)`
|
|
584
|
+
// shape is synthesized. A destructuring binding
|
|
585
|
+
// (`for (const {a,b} of ...)`) has no single flat target name
|
|
586
|
+
// lhsPath-style logic could bind to — left unsynthesized rather
|
|
587
|
+
// than guessed at, matching this codebase's "refuse rather than
|
|
588
|
+
// invent an edge" doctrine elsewhere (see _resolvableCalleeName
|
|
589
|
+
// in dataflow/engine.js for the same principle applied to call
|
|
590
|
+
// resolution).
|
|
591
|
+
const loopVar = declId && declId.type === 'Identifier' ? declId.name : null;
|
|
592
|
+
if (loopVar) {
|
|
593
|
+
const iterExpr = exprOf(path.node.right);
|
|
594
|
+
const bindId = nextNodeId();
|
|
595
|
+
addNode(fn, { id: bindId, kind: 'assign', target: loopVar, source: iterExpr, line, succ: [], pred: [] });
|
|
596
|
+
// `for (const x of ...)` / `for (let x of ...)` declares a
|
|
597
|
+
// BLOCK-SCOPED binding: a same-named `x` outside the loop is a
|
|
598
|
+
// different variable and must be completely unaffected by
|
|
599
|
+
// whatever the loop's `x` held. This engine's taint model has
|
|
600
|
+
// no block scoping, so the synthesized binding above would
|
|
601
|
+
// otherwise flow past the loop's exit and over-taint an outer
|
|
602
|
+
// `x` (proven: `let item='safe'; for (const item of req.body
|
|
603
|
+
// .items){} eval(item)` reported a Code Injection finding that
|
|
604
|
+
// pre-R13(b) code correctly reported as clean — the generic
|
|
605
|
+
// VariableDeclarator visitor used to emit `assign x <-
|
|
606
|
+
// {kind:'unknown'}` here, a taint KILL, and the guard above now
|
|
607
|
+
// suppresses it for exactly this shape). Record the name so
|
|
608
|
+
// exit() can re-emit that kill AFTER the loop, restoring the
|
|
609
|
+
// pre-existing behavior for post-loop reads while keeping the
|
|
610
|
+
// new in-loop taint flow.
|
|
611
|
+
//
|
|
612
|
+
// Deliberately NOT done for the bare-assignment form
|
|
613
|
+
// (`for (x of ...)`, leftNode is not a VariableDeclaration):
|
|
614
|
+
// that binding is not block-scoped, it reuses an existing
|
|
615
|
+
// outer `x`, and its value legitimately survives the loop —
|
|
616
|
+
// killing it there would itself be a regression.
|
|
617
|
+
if (leftNode && leftNode.type === 'VariableDeclaration') {
|
|
618
|
+
path.node._loopBindVar = loopVar;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
495
622
|
},
|
|
496
623
|
exit(path) {
|
|
497
624
|
const fn = currentFn(); if (!fn) return;
|
|
@@ -501,6 +628,26 @@ export function parseJsFile(file, code) {
|
|
|
501
628
|
linkCfg(fn, fn._cursor, headerId); // back-edge
|
|
502
629
|
linkCfg(fn, headerId, exitId); // exit edge
|
|
503
630
|
fn._cursor = exitId;
|
|
631
|
+
// See the _loopBindVar comment in enter(): restore the taint KILL
|
|
632
|
+
// that the ForOfStatement VariableDeclarator guard suppresses, so
|
|
633
|
+
// a block-scoped for-of binding's synthesized taint cannot leak
|
|
634
|
+
// past the loop onto a same-named outer variable. Emitted AFTER
|
|
635
|
+
// `fn._cursor = exitId` so it sits on the loop's normal exit path
|
|
636
|
+
// (header -> exit-noop -> kill -> whatever follows the loop) and
|
|
637
|
+
// therefore runs once on the way out — never inside the body, so
|
|
638
|
+
// in-loop taint reachability is unchanged.
|
|
639
|
+
if (path.node._loopBindVar) {
|
|
640
|
+
const killId = nextNodeId();
|
|
641
|
+
addNode(fn, {
|
|
642
|
+
id: killId,
|
|
643
|
+
kind: 'assign',
|
|
644
|
+
target: path.node._loopBindVar,
|
|
645
|
+
source: { kind: 'unknown' },
|
|
646
|
+
line: path.node.loc?.start?.line || 0,
|
|
647
|
+
succ: [],
|
|
648
|
+
pred: [],
|
|
649
|
+
});
|
|
650
|
+
}
|
|
504
651
|
},
|
|
505
652
|
},
|
|
506
653
|
|