circle-ir 4.16.0 → 4.17.0

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.
Files changed (59) hide show
  1. package/dist/analysis/constant-propagation/index.d.ts +2 -0
  2. package/dist/analysis/constant-propagation/index.d.ts.map +1 -1
  3. package/dist/analysis/constant-propagation/index.js +10 -0
  4. package/dist/analysis/constant-propagation/index.js.map +1 -1
  5. package/dist/analysis/constant-propagation/propagator.d.ts +11 -0
  6. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  7. package/dist/analysis/constant-propagation/propagator.js +125 -20
  8. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  9. package/dist/analysis/constant-propagation/types.d.ts +2 -0
  10. package/dist/analysis/constant-propagation/types.d.ts.map +1 -1
  11. package/dist/analysis/constant-propagation.d.ts +1 -1
  12. package/dist/analysis/constant-propagation.d.ts.map +1 -1
  13. package/dist/analysis/constant-propagation.js +1 -1
  14. package/dist/analysis/constant-propagation.js.map +1 -1
  15. package/dist/analysis/interprocedural.d.ts.map +1 -1
  16. package/dist/analysis/interprocedural.js +37 -8
  17. package/dist/analysis/interprocedural.js.map +1 -1
  18. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  19. package/dist/analysis/passes/cross-file-pass.js +3 -1
  20. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  21. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +13 -0
  22. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  23. package/dist/analysis/passes/csharp-sanitizer-credit.js +48 -0
  24. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  25. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  26. package/dist/analysis/passes/language-sources-pass.js +80 -8
  27. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  28. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  29. package/dist/analysis/passes/param-source-flow-gate-pass.js +17 -1
  30. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  31. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  32. package/dist/analysis/passes/sink-filter-pass.js +19 -3
  33. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  34. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  35. package/dist/analysis/passes/taint-propagation-pass.js +205 -22
  36. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  37. package/dist/analysis/rust-literal-guard.d.ts.map +1 -1
  38. package/dist/analysis/rust-literal-guard.js +100 -1
  39. package/dist/analysis/rust-literal-guard.js.map +1 -1
  40. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  41. package/dist/analysis/taint-matcher.js +1 -0
  42. package/dist/analysis/taint-matcher.js.map +1 -1
  43. package/dist/browser/circle-ir.js +561 -41
  44. package/dist/core/circle-ir-core.cjs +149 -17
  45. package/dist/core/circle-ir-core.js +149 -17
  46. package/dist/core/extractors/types.js +53 -9
  47. package/dist/core/extractors/types.js.map +1 -1
  48. package/dist/navigation/navigation-index.d.ts +40 -1
  49. package/dist/navigation/navigation-index.d.ts.map +1 -1
  50. package/dist/navigation/navigation-index.js +300 -45
  51. package/dist/navigation/navigation-index.js.map +1 -1
  52. package/dist/resolution/cross-file.d.ts +9 -0
  53. package/dist/resolution/cross-file.d.ts.map +1 -1
  54. package/dist/resolution/cross-file.js +134 -0
  55. package/dist/resolution/cross-file.js.map +1 -1
  56. package/dist/types/index.d.ts +18 -0
  57. package/dist/types/index.d.ts.map +1 -1
  58. package/dist/types/index.js.map +1 -1
  59. package/package.json +1 -1
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import { findJavaSpringReturnXSSSinks } from './language-sources-pass.js';
14
14
  import { expressionFullyWrappedBySanitizer, identifierOutsideSanitizerCalls, identifierOutsideSanitizerCallsInText, propagateTaint } from '../taint-propagation.js';
15
- import { isFalsePositive, isCorrelatedPredicateFP } from '../constant-propagation.js';
15
+ import { isFalsePositive, isCorrelatedPredicateFP, constPropScopedVar } from '../constant-propagation.js';
16
16
  import { buildJavaTaintedVars, buildPythonTaintedVars, buildRustTaintedVars, findPythonTrustBoundaryViolations, findPythonReturnXSSSinks, findJavaWikiPluginReturnXSSSinks } from './language-sources-pass.js';
17
17
  import { parseJavaArrayAccessExpression } from '../java-array-element-keys.js';
18
18
  import { canSourceReachSink, sourceSemanticsAllowed } from '../findings.js';
@@ -369,25 +369,74 @@ function resolveJavaDerivedAnchor(varName, derived, realSourceByVar, codeLines,
369
369
  * const-prop never seeds those derived names. Argument-style derivations
370
370
  * (`shown = encodeForPage(nick)`) must keep the veto so sanitizer credit holds.
371
371
  */
372
- function javaReceiverChainDerivedVar(code, localVar, seedVar) {
372
+ function javaReceiverChainDerivedVar(rhsByLocal, localVar, seedVar) {
373
373
  if (!seedVar || localVar === seedVar)
374
374
  return false;
375
+ const rhsList = rhsByLocal.get(localVar);
376
+ if (!rhsList)
377
+ return false;
375
378
  const esc = seedVar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
376
379
  const receiverOnRhs = new RegExp(`(?:^|[^\\w.$])${esc}\\s*\\.`, 'u');
380
+ return rhsList.some(rhs => receiverOnRhs.test(rhs));
381
+ }
382
+ /**
383
+ * cognium-dev#806 — the assignment RHS texts of each Java local, indexed once
384
+ * per file. `javaReceiverChainDerivedVar` used to re-run its declaration regex
385
+ * over every line for every (source, derived local) pair, which made large
386
+ * files take seconds. Same per-line matching, done once.
387
+ */
388
+ /** Line whose block scope owns `varName` as read on `useLine` (#476 / #597). */
389
+ function defLineForUse(graph, varName, useLine) {
390
+ const own = (graph.defsByLine.get(useLine) ?? []).find(d => d.variable === varName);
391
+ if (own)
392
+ return own.line;
393
+ const use = (graph.usesByLine.get(useLine) ?? []).find(u => u.variable === varName && u.def_id != null);
394
+ const d = use ? graph.defById.get(use.def_id) : undefined;
395
+ return d?.line ?? useLine;
396
+ }
397
+ /** Const-prop key for a flow step: block locals vs method-body locals (#476 / #597). */
398
+ function constPropFlowLookup(language, method, varName, blockLine, constProp, preferMethodScoped = false) {
399
+ if (language === 'go') {
400
+ return constPropScopedVar(method, varName, blockLine, constProp);
401
+ }
402
+ if (!method || varName.includes(':'))
403
+ return varName;
404
+ const blockScoped = constPropScopedVar(method, varName, blockLine, constProp);
405
+ const hasTaintBinding = (name) => constProp.tainted.has(name) || constProp.sanitizedVars.has(name);
406
+ if (blockScoped.includes(':b') && hasTaintBinding(blockScoped)) {
407
+ return blockScoped;
408
+ }
409
+ const methodScoped = `${method}:${varName}`;
410
+ if (hasTaintBinding(methodScoped)) {
411
+ return methodScoped;
412
+ }
413
+ if (preferMethodScoped && method) {
414
+ return methodScoped;
415
+ }
416
+ return constProp.lineBlockScopes?.has(blockLine) ? blockScoped : varName;
417
+ }
418
+ function indexJavaLocalAssignments(code) {
377
419
  const declRe = /^\s*(?:public|private|protected|static|final|volatile|transient|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
378
420
  const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
421
+ const index = new Map();
379
422
  for (const line of code.split('\n')) {
380
423
  const trimmed = line.trimStart();
381
424
  if (trimmed.startsWith('//') || trimmed.startsWith('*'))
382
425
  continue;
426
+ // Cheap pre-check: neither regex can match a line without `=` and `;`.
427
+ if (!line.includes('=') || !line.includes(';'))
428
+ continue;
383
429
  const m = declRe.exec(line) ?? assignRe.exec(line);
384
- if (!m || m[1] !== localVar)
430
+ if (!m)
385
431
  continue;
432
+ const list = index.get(m[1]);
386
433
  const rhs = stripJavaTrailingComment(m[2]);
387
- if (receiverOnRhs.test(rhs))
388
- return true;
434
+ if (list)
435
+ list.push(rhs);
436
+ else
437
+ index.set(m[1], [rhs]);
389
438
  }
390
- return false;
439
+ return index;
391
440
  }
392
441
  function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers) {
393
442
  const lines = typeof code === 'string' ? code.split('\n') : [];
@@ -423,15 +472,26 @@ function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers
423
472
  };
424
473
  return (step, sourceLine, sinkLine, sinkType) => {
425
474
  const method = methodAtLine(graph, step.line);
426
- const scopedVar = method ? `${method}:${step.variable}` : step.variable;
427
- // cognium-dev#751: Go locals are recorded method-scoped only (no unscoped
428
- // alias) by handleGoAssignment, so Go looks up the scoped name. Other
429
- // languages keep the unscoped lookup (#614).
430
- const r = isFalsePositive(constProp, step.line, language === 'go' ? scopedVar : step.variable);
475
+ const blockLine = defLineForUse(graph, step.variable, step.line);
476
+ const fpLookup = constPropFlowLookup(language, method, step.variable, blockLine, constProp);
477
+ const r = isFalsePositive(constProp, step.line, fpLookup);
431
478
  if (!r.isFalsePositive)
432
479
  return false;
433
480
  if (r.reason !== 'variable_not_tainted')
434
481
  return true;
482
+ if (r.reason === 'variable_not_tainted' &&
483
+ language === 'java' &&
484
+ method &&
485
+ fpLookup === step.variable &&
486
+ !step.variable.includes('.')) {
487
+ const v = step.variable;
488
+ if (constProp.tainted.has(`${method}:${v}`))
489
+ return false;
490
+ for (const t of constProp.tainted) {
491
+ if (t.startsWith(`${method}:b`) && t.endsWith(`:${v}`))
492
+ return false;
493
+ }
494
+ }
435
495
  // #589: the step variable came from a context-specific encoder
436
496
  // (`escapeHtml`, `encodeForHTML`, …), which does not clean `trust_boundary`
437
497
  // (a `session.setAttribute` sink). It is "not tainted" only for the other
@@ -447,8 +507,9 @@ function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers
447
507
  else {
448
508
  const method = methodAtLine(graph, step.line);
449
509
  if (method) {
450
- if (unc.get(`${method}:${step.variable}`)?.has(sinkType))
510
+ if (unc.get(fpLookup)?.has(sinkType)) {
451
511
  return false;
512
+ }
452
513
  }
453
514
  else if (unc.get(step.variable)?.has(sinkType)) {
454
515
  return false;
@@ -524,7 +585,8 @@ function makeConstPropVeto(constProp, graph, sources, code, language, sanitizers
524
585
  // cognium-dev#751: sink use after `uid := constantHelper(tainted)` must veto
525
586
  // when the helper is in `constantReturningMethods`, even if the flow's
526
587
  // source_line is the assignment line (rule E would otherwise keep it).
527
- if (step.line === sinkLine && !constProp.tainted.has(scopedVar)) {
588
+ if (step.line === sinkLine &&
589
+ !constProp.tainted.has(fpLookup)) {
528
590
  for (let ln = sourceLine; ln <= sinkLine; ln++) {
529
591
  const calleeMatch = (lines[ln - 1] ?? '').match(/:=\s*(\w+)\s*\(/);
530
592
  if (calleeMatch && constProp.constantReturningMethods?.has(calleeMatch[1])) {
@@ -553,16 +615,24 @@ export class TaintPropagationPass {
553
615
  // into const-prop's tainted set so DFG + expression-scan vetoes keep them.
554
616
  // Per-source seeding + receiver filter avoids argument-style derivations
555
617
  // (`shown = encodeForPage(nick)`) that must stay vetoed for sanitizer credit.
618
+ // cognium-dev#806: index the assignments once, visit each seed name once,
619
+ // and skip the file-wide derivation when no local not already tainted has
620
+ // a `seed.` receiver on its RHS (only such locals can be added).
556
621
  if (ctx.language === 'java' && typeof ctx.code === 'string') {
622
+ const rhsByLocal = indexJavaLocalAssignments(ctx.code);
623
+ const seen = new Set();
557
624
  for (const s of sources) {
558
- if (!s.variable)
625
+ if (!s.variable || seen.has(s.variable))
559
626
  continue;
560
- for (const [v] of buildJavaTaintedVars(ctx.code, new Set([s.variable]))) {
561
- if (v === s.variable)
562
- continue;
563
- if (javaReceiverChainDerivedVar(ctx.code, v, s.variable)) {
627
+ seen.add(s.variable);
628
+ const seedVar = s.variable;
629
+ const candidates = [...rhsByLocal.keys()].filter(v => !constProp.tainted.has(v) && javaReceiverChainDerivedVar(rhsByLocal, v, seedVar));
630
+ if (candidates.length === 0)
631
+ continue;
632
+ const derived = buildJavaTaintedVars(ctx.code, new Set([seedVar]));
633
+ for (const v of candidates) {
634
+ if (derived.has(v))
564
635
  constProp.tainted.add(v);
565
- }
566
636
  }
567
637
  }
568
638
  }
@@ -814,7 +884,7 @@ export class TaintPropagationPass {
814
884
  pushIfNew(f);
815
885
  }
816
886
  // Supplement: collection/iterator flows — with FP filtering
817
- const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
887
+ const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp, ctx.code, types, graph) ?? [];
818
888
  for (const f of collectionFlows) {
819
889
  if (flowKeys.has(flowKey(f)))
820
890
  continue;
@@ -896,6 +966,15 @@ export class TaintPropagationPass {
896
966
  pushIfNew(f);
897
967
  }
898
968
  }
969
+ // cognium-dev#686 — JS/TS module-scope `let`/`var` store-then-read across
970
+ // route handlers (variant xss stored). Mirrors #243 Go package globals.
971
+ if ((ctx.language === 'javascript' || ctx.language === 'typescript') &&
972
+ typeof ctx.code === 'string') {
973
+ const jsModuleFlows = detectJsModuleScopeFlows(ctx.code, sources, sinks, constProp.unreachableLines) ?? [];
974
+ for (const f of jsModuleFlows) {
975
+ pushIfNew(f);
976
+ }
977
+ }
899
978
  // Sprint 9 #58.1 — final pass: drop any flow whose source variable was
900
979
  // explicitly marked sanitized by a guard (e.g. regex-allowlist).
901
980
  // Applied to ALL flow generators (DFG-built and the four supplements)
@@ -1620,6 +1699,102 @@ function detectGoPackageGlobalFlows(code, _calls, sources, sinks, unreachableLin
1620
1699
  }
1621
1700
  return flows;
1622
1701
  }
1702
+ /**
1703
+ * cognium-dev#686 — module-level `let`/`var` written in one handler and read
1704
+ * in another. Per-function DFG cannot link these; text-scan supplement.
1705
+ */
1706
+ function detectJsModuleScopeFlows(code, sources, sinks, unreachableLines) {
1707
+ const flows = [];
1708
+ if (!code || sinks.length === 0)
1709
+ return flows;
1710
+ const lines = code.split('\n');
1711
+ const moduleVars = new Set();
1712
+ for (const line of lines) {
1713
+ const stripped = line.replace(/\s+$/, '');
1714
+ if (!stripped || /^[ \t]/.test(line))
1715
+ continue;
1716
+ const m = stripped.match(/^(?:let|var|const)\s+([A-Za-z_$][\w$]*)\b/);
1717
+ if (m)
1718
+ moduleVars.add(m[1]);
1719
+ }
1720
+ if (moduleVars.size === 0)
1721
+ return flows;
1722
+ const sourceVarNames = new Set();
1723
+ for (const s of sources) {
1724
+ if (typeof s.variable === 'string' && s.variable.length > 0) {
1725
+ sourceVarNames.add(s.variable);
1726
+ }
1727
+ }
1728
+ const JS_HTTP_SOURCE_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies|get)\b|\breq\.(?:param|query|body)\(/;
1729
+ const writes = [];
1730
+ for (let i = 0; i < lines.length; i++) {
1731
+ const line = lines[i];
1732
+ if (unreachableLines.has(i + 1))
1733
+ continue;
1734
+ for (const lhs of moduleVars) {
1735
+ const declRe = new RegExp(`\\b(?:let|const|var)\\s+${lhs.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`);
1736
+ if (declRe.test(line))
1737
+ continue;
1738
+ const m = line.match(new RegExp(`\\b${lhs.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*=\\s*([^;]+)`));
1739
+ if (!m)
1740
+ continue;
1741
+ const rhs = m[1];
1742
+ let taintedBy = null;
1743
+ const onLine = sources.filter(s => s.line === i + 1);
1744
+ if (onLine.length > 0) {
1745
+ taintedBy = { line: onLine[0].line, type: onLine[0].type };
1746
+ }
1747
+ else if (JS_HTTP_SOURCE_RE.test(rhs)) {
1748
+ taintedBy = { line: i + 1, type: 'http_body' };
1749
+ }
1750
+ else {
1751
+ for (const v of sourceVarNames) {
1752
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${v.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_$])`);
1753
+ if (re.test(rhs)) {
1754
+ const src = sources.find(s => s.variable === v);
1755
+ if (src) {
1756
+ taintedBy = { line: src.line, type: src.type };
1757
+ break;
1758
+ }
1759
+ }
1760
+ }
1761
+ }
1762
+ if (!taintedBy)
1763
+ continue;
1764
+ writes.push({ varName: lhs, line: i + 1, sourceLine: taintedBy.line, sourceType: taintedBy.type });
1765
+ }
1766
+ }
1767
+ if (writes.length === 0)
1768
+ return flows;
1769
+ for (const sink of sinks) {
1770
+ if (unreachableLines.has(sink.line))
1771
+ continue;
1772
+ const sinkLineText = lines[sink.line - 1] ?? '';
1773
+ if (!sinkLineText)
1774
+ continue;
1775
+ for (const w of writes) {
1776
+ if (w.line >= sink.line)
1777
+ continue;
1778
+ const re = new RegExp(`(?<![A-Za-z0-9_$])${w.varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?![A-Za-z0-9_$])`);
1779
+ if (!re.test(sinkLineText))
1780
+ continue;
1781
+ flows.push({
1782
+ source_line: w.sourceLine,
1783
+ sink_line: sink.line,
1784
+ source_type: w.sourceType,
1785
+ sink_type: sink.type,
1786
+ path: [
1787
+ { variable: w.varName, line: w.sourceLine, type: 'source' },
1788
+ { variable: w.varName, line: w.line, type: 'assignment' },
1789
+ { variable: w.varName, line: sink.line, type: 'sink' },
1790
+ ],
1791
+ confidence: 0.9,
1792
+ sanitized: false,
1793
+ });
1794
+ }
1795
+ }
1796
+ return flows;
1797
+ }
1623
1798
  /**
1624
1799
  * #614 — is `varName` tainted in the sink's own method?
1625
1800
  *
@@ -1726,7 +1901,9 @@ function isTaintedInMethod(taintedVars, varName, scopedName, fieldNames, isField
1726
1901
  }
1727
1902
  return true;
1728
1903
  }
1729
- function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code, types) {
1904
+ function detectCollectionFlows(calls, sources, sinks, constProp, code, types, graph) {
1905
+ const taintedVars = constProp.tainted;
1906
+ const unreachableLines = constProp.unreachableLines;
1730
1907
  const flows = [];
1731
1908
  const fieldNames = new Set();
1732
1909
  for (const t of types ?? [])
@@ -1751,7 +1928,13 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
1751
1928
  }
1752
1929
  if (arg.variable) {
1753
1930
  const varName = arg.variable;
1754
- const scopedName = call.in_method ? `${call.in_method}:${varName}` : varName;
1931
+ let scopedName = varName;
1932
+ if (call.in_method) {
1933
+ const blockLine = graph
1934
+ ? defLineForUse(graph, varName, call.location.line)
1935
+ : call.location.line;
1936
+ scopedName = constPropFlowLookup('java', call.in_method, varName, blockLine, constProp, true);
1937
+ }
1755
1938
  // Bare `name` that is a field read (no local of that name in this
1756
1939
  // method) still counts as a field use — Inter4-style. A shadowing
1757
1940
  // local declaration in the method does not.