circle-ir 4.14.2 → 4.15.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 (78) hide show
  1. package/dist/analysis/const-table-lookup.d.ts +29 -0
  2. package/dist/analysis/const-table-lookup.d.ts.map +1 -0
  3. package/dist/analysis/const-table-lookup.js +145 -0
  4. package/dist/analysis/const-table-lookup.js.map +1 -0
  5. package/dist/analysis/findings.d.ts.map +1 -1
  6. package/dist/analysis/findings.js +15 -4
  7. package/dist/analysis/findings.js.map +1 -1
  8. package/dist/analysis/go-strings-replacer.d.ts +21 -0
  9. package/dist/analysis/go-strings-replacer.d.ts.map +1 -0
  10. package/dist/analysis/go-strings-replacer.js +145 -0
  11. package/dist/analysis/go-strings-replacer.js.map +1 -0
  12. package/dist/analysis/interprocedural.d.ts.map +1 -1
  13. package/dist/analysis/interprocedural.js +53 -0
  14. package/dist/analysis/interprocedural.js.map +1 -1
  15. package/dist/analysis/java-canonical-prefix.d.ts +42 -0
  16. package/dist/analysis/java-canonical-prefix.d.ts.map +1 -0
  17. package/dist/analysis/java-canonical-prefix.js +145 -0
  18. package/dist/analysis/java-canonical-prefix.js.map +1 -0
  19. package/dist/analysis/java-crlf-strip.d.ts +30 -0
  20. package/dist/analysis/java-crlf-strip.d.ts.map +1 -0
  21. package/dist/analysis/java-crlf-strip.js +195 -0
  22. package/dist/analysis/java-crlf-strip.js.map +1 -0
  23. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts +2 -0
  24. package/dist/analysis/passes/cleartext-credential-transport-pass.d.ts.map +1 -1
  25. package/dist/analysis/passes/cleartext-credential-transport-pass.js +64 -12
  26. package/dist/analysis/passes/cleartext-credential-transport-pass.js.map +1 -1
  27. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  28. package/dist/analysis/passes/cross-file-pass.js +43 -0
  29. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  30. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -1
  31. package/dist/analysis/passes/csharp-sanitizer-credit.js +18 -0
  32. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -1
  33. package/dist/analysis/passes/interprocedural-pass.d.ts +28 -0
  34. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  35. package/dist/analysis/passes/interprocedural-pass.js +20 -1
  36. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  37. package/dist/analysis/passes/language-sources-pass.d.ts +41 -2
  38. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  39. package/dist/analysis/passes/language-sources-pass.js +842 -10
  40. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  41. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts +8 -0
  42. package/dist/analysis/passes/param-source-flow-gate-pass.d.ts.map +1 -1
  43. package/dist/analysis/passes/param-source-flow-gate-pass.js +162 -6
  44. package/dist/analysis/passes/param-source-flow-gate-pass.js.map +1 -1
  45. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  46. package/dist/analysis/passes/sink-filter-pass.js +207 -1
  47. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  48. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  49. package/dist/analysis/passes/taint-propagation-pass.js +257 -2
  50. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  51. package/dist/analysis/scalar-derivation.d.ts +25 -0
  52. package/dist/analysis/scalar-derivation.d.ts.map +1 -0
  53. package/dist/analysis/scalar-derivation.js +196 -0
  54. package/dist/analysis/scalar-derivation.js.map +1 -0
  55. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  56. package/dist/analysis/taint-matcher.js +167 -6
  57. package/dist/analysis/taint-matcher.js.map +1 -1
  58. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  59. package/dist/analysis/taint-propagation.js +149 -19
  60. package/dist/analysis/taint-propagation.js.map +1 -1
  61. package/dist/analyzer.d.ts.map +1 -1
  62. package/dist/analyzer.js +7 -0
  63. package/dist/analyzer.js.map +1 -1
  64. package/dist/browser/circle-ir.js +3140 -1186
  65. package/dist/core/circle-ir-core.cjs +485 -27
  66. package/dist/core/circle-ir-core.js +485 -27
  67. package/dist/core/extractors/dfg.js +240 -6
  68. package/dist/core/extractors/dfg.js.map +1 -1
  69. package/dist/graph/code-graph.d.ts +9 -0
  70. package/dist/graph/code-graph.d.ts.map +1 -1
  71. package/dist/graph/code-graph.js +11 -0
  72. package/dist/graph/code-graph.js.map +1 -1
  73. package/dist/resolution/cross-file.d.ts +7 -0
  74. package/dist/resolution/cross-file.d.ts.map +1 -1
  75. package/dist/resolution/cross-file.js +10 -1
  76. package/dist/resolution/cross-file.js.map +1 -1
  77. package/dist/wasm/web-tree-sitter.wasm +0 -0
  78. package/package.json +3 -3
@@ -9841,6 +9841,7 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9841
9841
  };
9842
9842
  defs.push(def);
9843
9843
  currentScope(scopeStack).set(name2, def.id);
9844
+ markDeclared(currentScope(scopeStack), name2);
9844
9845
  } else if (isJavaScript) {
9845
9846
  const destructDefs = extractDestructuringDefs(nameNode, defId);
9846
9847
  for (const def of destructDefs) {
@@ -9887,21 +9888,31 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9887
9888
  useId = condUses.nextId;
9888
9889
  }
9889
9890
  const consequence = stmt.childForFieldName("consequence");
9891
+ const alternative = stmt.childForFieldName("alternative");
9892
+ const topAlts = alternateDefs.get(currentScope(scopeStack));
9893
+ const before = /* @__PURE__ */ new Map();
9894
+ if (topAlts) for (const [name2, ids] of topAlts) before.set(name2, [...ids]);
9895
+ let armA = null;
9896
+ let armB = null;
9890
9897
  if (consequence) {
9891
- const result = processStatement2(consequence, defId, useId, scopeStack, isJavaScript);
9898
+ const result = processArm(consequence, defId, useId, scopeStack, isJavaScript);
9899
+ armA = result.popped;
9892
9900
  defs.push(...result.defs);
9893
9901
  uses.push(...result.uses);
9894
9902
  defId = result.nextDefId;
9895
9903
  useId = result.nextUseId;
9896
9904
  }
9897
- const alternative = stmt.childForFieldName("alternative");
9898
9905
  if (alternative) {
9899
- const result = processStatement2(alternative, defId, useId, scopeStack, isJavaScript);
9906
+ const result = processArm(alternative, defId, useId, scopeStack, isJavaScript);
9907
+ armB = result.popped;
9900
9908
  defs.push(...result.defs);
9901
9909
  uses.push(...result.uses);
9902
9910
  defId = result.nextDefId;
9903
9911
  useId = result.nextUseId;
9904
9912
  }
9913
+ if (armA && armB) {
9914
+ mergeIfElseDefs(armA, armB, before, scopeStack);
9915
+ }
9905
9916
  break;
9906
9917
  }
9907
9918
  case "for_statement":
@@ -9972,7 +9983,7 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
9972
9983
  }
9973
9984
  const body2 = stmt.childForFieldName("body");
9974
9985
  if (body2) {
9975
- const result = processStatement2(body2, defId, useId, scopeStack, isJavaScript);
9986
+ const result = processArm(body2, defId, useId, scopeStack, isJavaScript);
9976
9987
  defs.push(...result.defs);
9977
9988
  uses.push(...result.uses);
9978
9989
  defId = result.nextDefId;
@@ -10000,7 +10011,8 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
10000
10011
  defId = result.nextDefId;
10001
10012
  useId = result.nextUseId;
10002
10013
  }
10003
- scopeStack.pop();
10014
+ const poppedSwitch = scopeStack.pop();
10015
+ if (!isJavaScript) carryBranchDefsOut(poppedSwitch, scopeStack, false, true);
10004
10016
  }
10005
10017
  break;
10006
10018
  }
@@ -10012,7 +10024,10 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
10012
10024
  uses.push(...result.uses);
10013
10025
  defId = result.nextDefId;
10014
10026
  useId = result.nextUseId;
10015
- scopeStack.pop();
10027
+ const popped = scopeStack.pop();
10028
+ const alwaysRuns = stmt.parent?.type === "block" || stmt.parent?.type === "synchronized_statement";
10029
+ lastPoppedBlock = popped;
10030
+ if (!isJavaScript) carryBranchDefsOut(popped, scopeStack, alwaysRuns);
10016
10031
  break;
10017
10032
  }
10018
10033
  default: {
@@ -10054,6 +10069,7 @@ function processExpression(expr, startDefId, startUseId, scopeStack, isJavaScrip
10054
10069
  };
10055
10070
  defs.push(def);
10056
10071
  currentScope(scopeStack).set(name2, def.id);
10072
+ alternateDefs.get(currentScope(scopeStack))?.delete(name2);
10057
10073
  } else if (left) {
10058
10074
  const leftUses = extractUses(left, useId, scopeStack, isJavaScript);
10059
10075
  uses.push(...leftUses.uses);
@@ -10137,6 +10153,10 @@ function extractUses(node, startId, scopeStack, isJavaScript) {
10137
10153
  line: n.startPosition.row + 1,
10138
10154
  def_id: reachingDef
10139
10155
  });
10156
+ for (const alt of findAlternateDefs(name2, scopeStack)) {
10157
+ if (alt === reachingDef) continue;
10158
+ uses.push({ id: id++, variable: name2, line: n.startPosition.row + 1, def_id: alt });
10159
+ }
10140
10160
  }
10141
10161
  });
10142
10162
  return { uses, nextId: id };
@@ -10389,6 +10409,134 @@ function extractDestructuringDefs(node, startId) {
10389
10409
  function currentScope(scopeStack) {
10390
10410
  return scopeStack[scopeStack.length - 1];
10391
10411
  }
10412
+ var lastPoppedBlock = null;
10413
+ function inAlwaysRunBlock(arm) {
10414
+ let n = arm.parent;
10415
+ while (n && (n.type === "if_statement" || n.type === "for_statement" || n.type === "enhanced_for_statement" || n.type === "while_statement" || n.type === "do_statement")) n = n.parent;
10416
+ return !!n && n.type === "block" && !!n.parent && (n.parent.type === "block" || n.parent.type === "synchronized_statement");
10417
+ }
10418
+ function inNestedBlock(arm) {
10419
+ let n = arm.parent;
10420
+ while (n && (n.type === "if_statement" || n.type === "for_statement" || n.type === "enhanced_for_statement" || n.type === "while_statement" || n.type === "do_statement")) n = n.parent;
10421
+ if (!n || n.type !== "block" || !n.parent) return false;
10422
+ switch (n.parent.type) {
10423
+ case "block":
10424
+ case "synchronized_statement":
10425
+ case "if_statement":
10426
+ case "for_statement":
10427
+ case "enhanced_for_statement":
10428
+ case "while_statement":
10429
+ case "do_statement":
10430
+ case "try_statement":
10431
+ case "try_with_resources_statement":
10432
+ case "catch_clause":
10433
+ case "finally_clause":
10434
+ case "labeled_statement":
10435
+ return true;
10436
+ default:
10437
+ return false;
10438
+ }
10439
+ }
10440
+ function processArm(node, defId, useId, scopeStack, isJavaScript) {
10441
+ const isBlock = node.type === "block" || node.type === "statement_block";
10442
+ if (isJavaScript || (isBlock ? !inAlwaysRunBlock(node) : !inNestedBlock(node))) {
10443
+ lastPoppedBlock = null;
10444
+ const result2 = processStatement2(node, defId, useId, scopeStack, isJavaScript);
10445
+ return { ...result2, popped: null };
10446
+ }
10447
+ if (isBlock) {
10448
+ lastPoppedBlock = null;
10449
+ const result2 = processStatement2(node, defId, useId, scopeStack, isJavaScript);
10450
+ return { ...result2, popped: lastPoppedBlock };
10451
+ }
10452
+ scopeStack.push(/* @__PURE__ */ new Map());
10453
+ const result = processStatement2(node, defId, useId, scopeStack, isJavaScript);
10454
+ const popped = scopeStack.pop();
10455
+ carryBranchDefsOut(popped, scopeStack, false);
10456
+ return { ...result, popped };
10457
+ }
10458
+ function mergeIfElseDefs(armA, armB, before, scopeStack) {
10459
+ const top = scopeStack[scopeStack.length - 1];
10460
+ if (!top) return;
10461
+ const alts = alternateDefs.get(top);
10462
+ for (const [name2, idA] of armA) {
10463
+ const idB = armB.get(name2);
10464
+ if (idB === void 0) continue;
10465
+ if (declaredNames.get(armA)?.has(name2) || declaredNames.get(armB)?.has(name2)) continue;
10466
+ if (!scopeStack.some((scope) => scope.has(name2))) continue;
10467
+ const prior = new Set(before.get(name2) ?? []);
10468
+ const arms = (alts?.get(name2) ?? []).filter((id) => !prior.has(id) && id !== idB);
10469
+ if (!arms.includes(idA)) arms.push(idA);
10470
+ top.set(name2, idB);
10471
+ let map = alts;
10472
+ if (!map) {
10473
+ map = /* @__PURE__ */ new Map();
10474
+ alternateDefs.set(top, map);
10475
+ }
10476
+ map.set(name2, arms);
10477
+ }
10478
+ }
10479
+ var declaredNames = /* @__PURE__ */ new WeakMap();
10480
+ var alternateDefs = /* @__PURE__ */ new WeakMap();
10481
+ function markDeclared(scope, name2) {
10482
+ let set = declaredNames.get(scope);
10483
+ if (!set) {
10484
+ set = /* @__PURE__ */ new Set();
10485
+ declaredNames.set(scope, set);
10486
+ }
10487
+ set.add(name2);
10488
+ alternateDefs.get(scope)?.delete(name2);
10489
+ }
10490
+ function carryBranchDefsOut(popped, scopeStack, alwaysRuns = false, altsOnly = false) {
10491
+ const declared = declaredNames.get(popped);
10492
+ const poppedAlts = alternateDefs.get(popped);
10493
+ const top = scopeStack[scopeStack.length - 1];
10494
+ if (!top) return;
10495
+ if (alwaysRuns) {
10496
+ for (const [name2, id] of popped) {
10497
+ if (declared?.has(name2)) continue;
10498
+ if (!scopeStack.some((scope) => scope.has(name2))) continue;
10499
+ top.set(name2, id);
10500
+ let alts = alternateDefs.get(top);
10501
+ if (!alts) {
10502
+ alts = /* @__PURE__ */ new Map();
10503
+ alternateDefs.set(top, alts);
10504
+ }
10505
+ alts.delete(name2);
10506
+ const kept = poppedAlts?.get(name2);
10507
+ if (kept && kept.length > 0) alts.set(name2, [...kept]);
10508
+ }
10509
+ if (poppedAlts) {
10510
+ for (const [name2, ids] of poppedAlts) if (!popped.has(name2)) carry(name2, ids);
10511
+ }
10512
+ return;
10513
+ }
10514
+ function carry(name2, ids) {
10515
+ if (declared?.has(name2)) return;
10516
+ if (!scopeStack.some((scope) => scope.has(name2))) return;
10517
+ let alts = alternateDefs.get(top);
10518
+ if (!alts) {
10519
+ alts = /* @__PURE__ */ new Map();
10520
+ alternateDefs.set(top, alts);
10521
+ }
10522
+ const list = alts.get(name2) ?? [];
10523
+ for (const id of ids) if (!list.includes(id)) list.push(id);
10524
+ alts.set(name2, list);
10525
+ }
10526
+ if (!altsOnly) for (const [name2, id] of popped) carry(name2, [id]);
10527
+ if (poppedAlts) for (const [name2, ids] of poppedAlts) carry(name2, ids);
10528
+ }
10529
+ function findAlternateDefs(name2, scopeStack) {
10530
+ const out2 = [];
10531
+ for (let i2 = scopeStack.length - 1; i2 >= 0; i2--) {
10532
+ const alts = alternateDefs.get(scopeStack[i2])?.get(name2);
10533
+ if (alts) {
10534
+ for (const id of alts) if (!out2.includes(id)) out2.push(id);
10535
+ }
10536
+ if (scopeStack[i2].has(name2)) break;
10537
+ }
10538
+ return out2;
10539
+ }
10392
10540
  function findReachingDef(name2, scopeStack) {
10393
10541
  for (let i2 = scopeStack.length - 1; i2 >= 0; i2--) {
10394
10542
  const defId = scopeStack[i2].get(name2);
@@ -14745,6 +14893,157 @@ function getDefaultConfig() {
14745
14893
  };
14746
14894
  }
14747
14895
 
14896
+ // src/analysis/java-crlf-strip.ts
14897
+ function decodeJavaLiteral(raw) {
14898
+ return raw.replace(/\\(?:u+([0-9a-fA-F]{4})|([0-3][0-7]{0,2}|[4-7][0-7]?)|([btnfrs"'\\]))/g, (_m, hex, oct, simple) => {
14899
+ if (hex !== void 0) return String.fromCharCode(parseInt(hex, 16));
14900
+ if (oct !== void 0) return String.fromCharCode(parseInt(oct, 8));
14901
+ switch (simple) {
14902
+ case "b":
14903
+ return "\b";
14904
+ case "t":
14905
+ return " ";
14906
+ case "n":
14907
+ return "\n";
14908
+ case "f":
14909
+ return "\f";
14910
+ case "r":
14911
+ return "\r";
14912
+ case "s":
14913
+ return " ";
14914
+ default:
14915
+ return simple;
14916
+ }
14917
+ });
14918
+ }
14919
+ function readAtom(s, i2) {
14920
+ const c = s[i2];
14921
+ if (c === "\\" && i2 + 1 < s.length) {
14922
+ const n = s[i2 + 1];
14923
+ return { atom: n === "r" ? "cr" : n === "n" ? "lf" : "other", end: i2 + 2 };
14924
+ }
14925
+ return { atom: c === "\r" ? "cr" : c === "\n" ? "lf" : "other", end: i2 + 1 };
14926
+ }
14927
+ function quantifierAllowsOne(q) {
14928
+ if (q === "") return true;
14929
+ const m = /^(?:(\*)|(\+)|(\?)|\{(\d+)(?:(,)(\d*))?\})[+?]?$/.exec(q);
14930
+ if (!m) return false;
14931
+ if (m[1] || m[2] || m[3]) return true;
14932
+ const min = Number(m[4]);
14933
+ const max = m[5] ? m[6] === "" ? Infinity : Number(m[6]) : min;
14934
+ return min <= 1 && max >= 1;
14935
+ }
14936
+ var NONE = { cr: false, lf: false };
14937
+ function alternativeCoverage(alt) {
14938
+ if (alt === "") return NONE;
14939
+ let cr = false;
14940
+ let lf = false;
14941
+ let i2;
14942
+ if (alt[0] === "[") {
14943
+ if (alt[1] === "^") return NONE;
14944
+ i2 = 1;
14945
+ let closed = false;
14946
+ while (i2 < alt.length) {
14947
+ if (alt[i2] === "]" && i2 > 1) {
14948
+ closed = true;
14949
+ i2++;
14950
+ break;
14951
+ }
14952
+ if (alt[i2] === "[" || alt[i2] === "&" && alt[i2 + 1] === "&") return NONE;
14953
+ const { atom, end } = readAtom(alt, i2);
14954
+ if (atom === "cr") cr = true;
14955
+ else if (atom === "lf") lf = true;
14956
+ i2 = end;
14957
+ }
14958
+ if (!closed) return NONE;
14959
+ } else {
14960
+ const { atom, end } = readAtom(alt, 0);
14961
+ if (atom === "other") return NONE;
14962
+ cr = atom === "cr";
14963
+ lf = atom === "lf";
14964
+ i2 = end;
14965
+ }
14966
+ return quantifierAllowsOne(alt.slice(i2)) ? { cr, lf } : NONE;
14967
+ }
14968
+ function splitAlternatives(s) {
14969
+ const alts = [];
14970
+ let depth = 0;
14971
+ let inClass = false;
14972
+ let cur = "";
14973
+ for (let i2 = 0; i2 < s.length; i2++) {
14974
+ const c = s[i2];
14975
+ if (c === "\\") {
14976
+ cur += c + (s[i2 + 1] ?? "");
14977
+ i2++;
14978
+ continue;
14979
+ }
14980
+ if (inClass) {
14981
+ if (c === "]") inClass = false;
14982
+ cur += c;
14983
+ continue;
14984
+ }
14985
+ if (c === "[") {
14986
+ inClass = true;
14987
+ cur += c;
14988
+ continue;
14989
+ }
14990
+ if (c === "(") depth++;
14991
+ else if (c === ")") depth--;
14992
+ if (c === "|" && depth === 0) {
14993
+ alts.push(cur);
14994
+ cur = "";
14995
+ continue;
14996
+ }
14997
+ cur += c;
14998
+ }
14999
+ alts.push(cur);
15000
+ return alts;
15001
+ }
15002
+ function unwrapGroup(s) {
15003
+ const open = /^\((?:\?:)?/.exec(s);
15004
+ if (!open) return null;
15005
+ let depth = 0;
15006
+ let inClass = false;
15007
+ for (let i2 = 0; i2 < s.length; i2++) {
15008
+ const c = s[i2];
15009
+ if (c === "\\") {
15010
+ i2++;
15011
+ continue;
15012
+ }
15013
+ if (inClass) {
15014
+ if (c === "]") inClass = false;
15015
+ continue;
15016
+ }
15017
+ if (c === "[") {
15018
+ inClass = true;
15019
+ continue;
15020
+ }
15021
+ if (c === "(") depth++;
15022
+ else if (c === ")" && --depth === 0) {
15023
+ return { body: s.slice(open[0].length, i2), quantifier: s.slice(i2 + 1) };
15024
+ }
15025
+ }
15026
+ return null;
15027
+ }
15028
+ function javaReplaceCrLfCoverage(method, rawFrom) {
15029
+ const value = decodeJavaLiteral(rawFrom);
15030
+ if (method === "replace") return { cr: value === "\r", lf: value === "\n" };
15031
+ let body2 = value;
15032
+ const group = unwrapGroup(value);
15033
+ if (group) {
15034
+ if (!quantifierAllowsOne(group.quantifier)) return NONE;
15035
+ body2 = group.body;
15036
+ }
15037
+ let cr = false;
15038
+ let lf = false;
15039
+ for (const alt of splitAlternatives(body2)) {
15040
+ const c = alternativeCoverage(alt);
15041
+ if (c.cr) cr = true;
15042
+ if (c.lf) lf = true;
15043
+ }
15044
+ return { cr, lf };
15045
+ }
15046
+
14748
15047
  // src/analysis/arg-type-resolver.ts
14749
15048
  function findEnclosingType(callInMethod, types) {
14750
15049
  if (!callInMethod) return null;
@@ -14845,7 +15144,7 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
14845
15144
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
14846
15145
  const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
14847
15146
  const gatedSinks = language === "csharp" ? dropCSharpObjectCarriedWaypoints(sinks, calls) : sinks;
14848
- const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
15147
+ const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines, language);
14849
15148
  return { sources, sinks: gatedSinks, sanitizers };
14850
15149
  }
14851
15150
  function sinkPatternAppliesTo(pattern, language) {
@@ -15864,6 +16163,25 @@ function isGoFprintToNonResponseWriter(call, pattern, language, sourceLines) {
15864
16163
  }
15865
16164
  return true;
15866
16165
  }
16166
+ var JAVA_OUTBOUND_STREAM_OWNERS = "HttpURLConnection|HttpsURLConnection|URLConnection|JarURLConnection|Socket|SSLSocket|Process";
16167
+ function isJavaOutboundOutputStreamWrite(call, pattern, language, sourceLines) {
16168
+ if (language !== "java" || pattern.type !== "xss") return false;
16169
+ const receiver = (call.receiver ?? "").replace(/\s+/g, "");
16170
+ if (!receiver.endsWith(".getOutputStream()")) return false;
16171
+ if (/\.openConnection\(\)\)?\.getOutputStream\(\)$/.test(receiver)) return true;
16172
+ const m = receiver.match(/^\(?([A-Za-z_]\w*)\)?\.getOutputStream\(\)$/);
16173
+ if (!m || !sourceLines) return false;
16174
+ const declRe = new RegExp(
16175
+ `\\b(?:[\\w.]+\\.)?(?:${JAVA_OUTBOUND_STREAM_OWNERS})\\s+${m[1]}\\s*[=;,)]`
16176
+ );
16177
+ const anyDeclRe = new RegExp(`\\b(?:[A-Z][\\w.<>,\\[\\]]*|var)\\s+${m[1]}\\s*[=;,)]`);
16178
+ for (let i2 = call.location.line - 1; i2 >= 0; i2--) {
16179
+ const line = (sourceLines[i2] ?? "").replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/.*$/, "");
16180
+ if (declRe.test(line)) return true;
16181
+ if (anyDeclRe.test(line)) return false;
16182
+ }
16183
+ return false;
16184
+ }
15867
16185
  function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
15868
16186
  if (language !== "go") return false;
15869
16187
  if (pattern.type !== "deserialization") return false;
@@ -16013,6 +16331,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
16013
16331
  if (isGoFprintToNonResponseWriter(call, pattern, language, sourceLines)) {
16014
16332
  continue;
16015
16333
  }
16334
+ if (isJavaOutboundOutputStreamWrite(call, pattern, language, sourceLines)) {
16335
+ continue;
16336
+ }
16016
16337
  if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
16017
16338
  continue;
16018
16339
  }
@@ -16420,6 +16741,13 @@ function receiverMightBeClassImpl(receiver, className) {
16420
16741
  if (receiver === className) {
16421
16742
  return true;
16422
16743
  }
16744
+ const inlineRequire = receiver.match(/^require\(\s*['"`](?:node:)?([\w./-]+)['"`]\s*\)$/);
16745
+ if (inlineRequire && inlineRequire[1] === className) {
16746
+ return true;
16747
+ }
16748
+ if (className === "ClassLoader" && /(?:^|\.)(?:getContextClassLoader|getClassLoader|getSystemClassLoader)\(\s*\)$/.test(receiver.replace(/\s+/g, ""))) {
16749
+ return true;
16750
+ }
16423
16751
  if (receiver.endsWith(")")) {
16424
16752
  const ctorMatch = receiver.match(/^(\w+)\(/);
16425
16753
  if (ctorMatch) {
@@ -16708,8 +17036,9 @@ function calculateSinkConfidence(call, pattern) {
16708
17036
  }
16709
17037
  return Math.min(confidence, 1);
16710
17038
  }
16711
- function findSanitizers(calls, types, patterns, sourceLines) {
17039
+ function findSanitizers(calls, types, patterns, sourceLines, language) {
16712
17040
  const sanitizers = [];
17041
+ const javaReplace = (call) => language === "java" && sourceLines && (call.method_name === "replace" || call.method_name === "replaceAll") ? javaReplaceChainCredit(javaChainTextAt(sourceLines, call.location.line, call.location.column)) : null;
16713
17042
  const sanitizerMethods = /* @__PURE__ */ new Set();
16714
17043
  for (const type of types) {
16715
17044
  for (const method of type.methods) {
@@ -16734,10 +17063,15 @@ function findSanitizers(calls, types, patterns, sourceLines) {
16734
17063
  if (inside.length !== 1) continue;
16735
17064
  const innerCall = inside[0];
16736
17065
  let matched;
16737
- for (const pattern of patterns) {
16738
- if (matchesSanitizerPattern(innerCall, pattern)) {
16739
- matched = pattern;
16740
- break;
17066
+ const replaceCredit = javaReplace(innerCall);
17067
+ if (replaceCredit) {
17068
+ matched = replaceCredit.length > 0 ? { method: innerCall.method_name, removes: replaceCredit } : void 0;
17069
+ } else {
17070
+ for (const pattern of patterns) {
17071
+ if (matchesSanitizerPattern(innerCall, pattern)) {
17072
+ matched = pattern;
17073
+ break;
17074
+ }
16741
17075
  }
16742
17076
  }
16743
17077
  if (!matched || !matched.removes || matched.removes.length === 0) continue;
@@ -16789,6 +17123,18 @@ function findSanitizers(calls, types, patterns, sourceLines) {
16789
17123
  });
16790
17124
  continue;
16791
17125
  }
17126
+ const replaceCredit = javaReplace(call);
17127
+ if (replaceCredit) {
17128
+ if (replaceCredit.length > 0) {
17129
+ sanitizers.push({
17130
+ type: "method_call",
17131
+ method: formatSanitizerMethod(call),
17132
+ line: call.location.line,
17133
+ sanitizes: replaceCredit
17134
+ });
17135
+ }
17136
+ continue;
17137
+ }
16792
17138
  for (const pattern of patterns) {
16793
17139
  if (matchesSanitizerPattern(call, pattern)) {
16794
17140
  sanitizers.push({
@@ -16818,6 +17164,52 @@ function findSanitizers(calls, types, patterns, sourceLines) {
16818
17164
  }
16819
17165
  return sanitizers;
16820
17166
  }
17167
+ var JAVA_LIT = String.raw`(?:"((?:\\.|[^"\\])*)"|'((?:\\.|[^'\\])*)')`;
17168
+ var JAVA_REPLACE_ARGS_RE = new RegExp(String.raw`\.(replace|replaceAll)\(\s*${JAVA_LIT}\s*,\s*${JAVA_LIT}\s*\)`, "g");
17169
+ function javaChainTextAt(sourceLines, line, column) {
17170
+ const text = [(sourceLines[line - 1] ?? "").slice(column), ...sourceLines.slice(line, line + 7)].join("\n");
17171
+ let depth = 0;
17172
+ let i2 = 0;
17173
+ for (; i2 < text.length; i2++) {
17174
+ const c = text[i2];
17175
+ if (c === '"' || c === "'") {
17176
+ for (i2++; i2 < text.length && text[i2] !== c; i2++) if (text[i2] === "\\") i2++;
17177
+ continue;
17178
+ }
17179
+ if (c === "(" || c === "[") {
17180
+ depth++;
17181
+ continue;
17182
+ }
17183
+ if (c === ")" || c === "]") {
17184
+ if (depth === 0) break;
17185
+ depth--;
17186
+ continue;
17187
+ }
17188
+ if (depth === 0 && !/[\w$.\s]/.test(c)) break;
17189
+ }
17190
+ return text.slice(0, i2);
17191
+ }
17192
+ function javaReplaceChainCredit(chainText) {
17193
+ let cr = false;
17194
+ let lf = false;
17195
+ let lt = false;
17196
+ let gt = false;
17197
+ for (const m of chainText.matchAll(JAVA_REPLACE_ARGS_RE)) {
17198
+ const kind = m[1];
17199
+ const rawFrom = m[2] ?? m[3] ?? "";
17200
+ const from = rawFrom.replace(/\\\\/g, "\\");
17201
+ const to = m[4] ?? m[5] ?? "";
17202
+ const cov = javaReplaceCrLfCoverage(kind, rawFrom);
17203
+ if (cov.cr) cr = true;
17204
+ if (cov.lf) lf = true;
17205
+ if (from === "<" && to.startsWith("&")) lt = true;
17206
+ if (from === ">" && to.startsWith("&")) gt = true;
17207
+ }
17208
+ const out2 = [];
17209
+ if (cr && lf) out2.push("log_injection");
17210
+ if (lt && gt) out2.push("xss");
17211
+ return out2;
17212
+ }
16821
17213
  function matchesSanitizerPattern(call, pattern) {
16822
17214
  if (pattern.method) {
16823
17215
  if (call.method_name !== pattern.method) {
@@ -16858,6 +17250,15 @@ function formatSanitizerMethod(call) {
16858
17250
  // src/graph/code-graph.ts
16859
17251
  var CodeGraph = class {
16860
17252
  ir;
17253
+ /**
17254
+ * Defs whose value cannot carry the content of the values they read, so
17255
+ * taint does not flow into them. Set by `analyze()`; empty for a bare graph.
17256
+ * - an int or bool measure or test (`len(x)`, `x.length`,
17257
+ * `re.MatchString(x)`) (#568)
17258
+ * - a lookup in a constant table the file never writes (`T[req.query.k]`)
17259
+ * (#667)
17260
+ */
17261
+ taintBarrierDefIds = /* @__PURE__ */ new Set();
16861
17262
  constructor(ir) {
16862
17263
  this.ir = ir;
16863
17264
  }
@@ -17112,6 +17513,7 @@ var CodeGraph = class {
17112
17513
  for (const [fromDef, chains] of this.chainsByFromDef) {
17113
17514
  if (!result.has(fromDef)) continue;
17114
17515
  for (const chain of chains) {
17516
+ if (this.taintBarrierDefIds.has(chain.to_def)) continue;
17115
17517
  if (!result.has(chain.to_def)) {
17116
17518
  result.add(chain.to_def);
17117
17519
  changed = true;
@@ -17197,6 +17599,10 @@ function buildSanitizersByLine(sanitizers) {
17197
17599
  }
17198
17600
  return out2;
17199
17601
  }
17602
+ function templateInterpolationCount(expression) {
17603
+ if (!expression.trimStart().startsWith("`")) return 0;
17604
+ return (expression.match(/\$\{/g) ?? []).length;
17605
+ }
17200
17606
  function concatOutsideStrings(expression) {
17201
17607
  return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
17202
17608
  }
@@ -17232,31 +17638,38 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17232
17638
  const taintedVars = [];
17233
17639
  const flows = [];
17234
17640
  const javaConcat = graph.ir.meta.language === "java";
17641
+ const jsLike = graph.ir.meta.language === "javascript" || graph.ir.meta.language === "typescript";
17235
17642
  const reachableSinks = /* @__PURE__ */ new Map();
17236
17643
  const defsByLine = graph.defsByLine;
17237
17644
  const usesByLine = graph.usesByLine;
17238
17645
  const callsByLine = graph.callsByLine;
17239
17646
  const sanitizersByLine = sanitizers.length > 0 ? buildSanitizersByLine(sanitizers) : graph.sanitizersByLine;
17240
17647
  const defById = graph.defById;
17241
- const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine);
17242
- const initialTaint = rawInitialTaint.filter((tv) => {
17648
+ const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine, graph.chainsByFromDef, jsLike);
17649
+ const filteredInitialTaint = rawInitialTaint.filter((tv) => {
17243
17650
  if (tv.line === tv.sourceLine) return true;
17651
+ if (graph.taintBarrierDefIds.has(tv.defId)) return false;
17244
17652
  const sanCheck = checkSanitized(tv.sourceLine, tv.line, tv.sourceType, sanitizersByLine);
17245
17653
  return !sanCheck.sanitized;
17246
17654
  });
17655
+ const initialTaint = [
17656
+ ...filteredInitialTaint.filter((tv) => tv.sourceType !== "interprocedural_param"),
17657
+ ...filteredInitialTaint.filter((tv) => tv.sourceType === "interprocedural_param")
17658
+ ];
17247
17659
  taintedVars.push(...initialTaint);
17248
17660
  const propagatedTaint = propagateThroughChains(
17249
17661
  initialTaint,
17250
17662
  graph.chainsByFromDef,
17251
17663
  defById,
17252
- sanitizersByLine
17664
+ sanitizersByLine,
17665
+ graph.taintBarrierDefIds
17253
17666
  );
17254
17667
  taintedVars.push(...propagatedTaint);
17255
17668
  const allTaintedDefIds = /* @__PURE__ */ new Set();
17256
17669
  const taintByDefId = /* @__PURE__ */ new Map();
17257
17670
  for (const tv of taintedVars) {
17258
17671
  allTaintedDefIds.add(tv.defId);
17259
- taintByDefId.set(tv.defId, tv);
17672
+ if (!yieldsToRealSource(taintByDefId.get(tv.defId), tv)) taintByDefId.set(tv.defId, tv);
17260
17673
  }
17261
17674
  for (const sink of sinks) {
17262
17675
  const usesAtSink = usesByLine.get(sink.line) ?? [];
@@ -17269,7 +17682,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17269
17682
  }
17270
17683
  }
17271
17684
  const expr = typeof arg.expression === "string" ? arg.expression : "";
17272
- const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
17685
+ const compoundConcat = !!arg.variable && expr !== arg.variable && (javaConcat && concatOutsideStrings(expr) || jsLike && templateInterpolationCount(expr) > 1);
17273
17686
  const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
17274
17687
  {
17275
17688
  for (const use of candidateUses) {
@@ -17342,12 +17755,46 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17342
17755
  }
17343
17756
  return { taintedVars, flows: deduped, reachableSinks };
17344
17757
  }
17345
- function findInitialTaint(sources, callsByLine, defsByLine) {
17758
+ var PARAM_SCOPED_SOURCE_RE = /^(?:GraphQL \w+\.\w+ resolver args|Express handler destructured request) /;
17759
+ function isCompleteConditionHeader(code) {
17760
+ if (!code) return false;
17761
+ const m = /^(?:\}\s*)?(?:else\s+)?(?:if|while)\s*\(/.exec(code);
17762
+ if (!m) return false;
17763
+ const text = code.slice(m[0].length - 1).replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g, '""');
17764
+ let depth = 0;
17765
+ for (const ch of text) {
17766
+ if (ch === "(") depth++;
17767
+ else if (ch === ")" && --depth === 0) return true;
17768
+ }
17769
+ return false;
17770
+ }
17771
+ function hasSecondStatement(code) {
17772
+ if (!code || /^for\b/.test(code)) return false;
17773
+ const text = code.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g, '""');
17774
+ return /;\s*(?!\/\/|\/\*|#|\}|\)|$)\S/.test(text);
17775
+ }
17776
+ function findInitialTaint(sources, callsByLine, defsByLine, chainsByFromDef, jsLike = false) {
17346
17777
  const tainted = [];
17347
17778
  for (const source of sources) {
17348
17779
  const defsOnLine = defsByLine.get(source.line) ?? [];
17780
+ let seedable = null;
17781
+ if (source.variable && hasSecondStatement(source.code)) {
17782
+ const own = defsOnLine.filter((d) => d.variable === source.variable);
17783
+ if (own.length > 0) {
17784
+ seedable = new Set(own.map((d) => d.id));
17785
+ for (const d of own) {
17786
+ for (const c of chainsByFromDef.get(d.id) ?? []) {
17787
+ if (defsOnLine.some((o) => o.id === c.to_def)) seedable.add(c.to_def);
17788
+ }
17789
+ }
17790
+ }
17791
+ }
17349
17792
  for (const def of defsOnLine) {
17350
- if (source.type === "interprocedural_param" && source.variable && def.kind === "param" && def.variable !== source.variable) {
17793
+ if (seedable && !seedable.has(def.id)) continue;
17794
+ if ((source.type === "interprocedural_param" || PARAM_SCOPED_SOURCE_RE.test(source.location ?? "")) && source.variable && def.kind === "param" && def.variable !== source.variable) {
17795
+ continue;
17796
+ }
17797
+ if (source.type === "interprocedural_param" && def.kind !== "param") {
17351
17798
  continue;
17352
17799
  }
17353
17800
  tainted.push({
@@ -17359,7 +17806,8 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
17359
17806
  confidence: source.confidence
17360
17807
  });
17361
17808
  }
17362
- const defsNextLine = defsByLine.get(source.line + 1) ?? [];
17809
+ const closesOwnStatement = jsLike && defsOnLine.length > 0 && /;\s*(?:\/\/.*)?$/.test(source.code ?? "");
17810
+ const defsNextLine = isCompleteConditionHeader(source.code) ? [] : (defsByLine.get(source.line + 1) ?? []).filter((d) => !(closesOwnStatement && d.kind === "local"));
17363
17811
  for (const def of defsNextLine) {
17364
17812
  const callsOnSourceLine = callsByLine.get(source.line) ?? [];
17365
17813
  if (callsOnSourceLine.length > 0) {
@@ -17393,12 +17841,15 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
17393
17841
  }
17394
17842
  return tainted;
17395
17843
  }
17396
- function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitizersByLine) {
17844
+ function yieldsToRealSource(existing, next) {
17845
+ return existing !== void 0 && next.sourceType === "interprocedural_param" && existing.sourceType !== "interprocedural_param";
17846
+ }
17847
+ function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitizersByLine, taintBarrierDefIds) {
17397
17848
  const propagated = [];
17398
17849
  const taintedDefIds = new Set(initialTaint.map((t) => t.defId));
17399
17850
  const taintInfoByDefId = /* @__PURE__ */ new Map();
17400
17851
  for (const t of initialTaint) {
17401
- taintInfoByDefId.set(t.defId, t);
17852
+ if (!yieldsToRealSource(taintInfoByDefId.get(t.defId), t)) taintInfoByDefId.set(t.defId, t);
17402
17853
  }
17403
17854
  const queue = [...initialTaint.map((t) => t.defId)];
17404
17855
  const visited = new Set(queue);
@@ -17409,6 +17860,7 @@ function propagateThroughChains(initialTaint, chainsByFromDef, defById, sanitize
17409
17860
  const outgoingChains = chainsByFromDef.get(currentDefId) ?? [];
17410
17861
  for (const chain of outgoingChains) {
17411
17862
  if (visited.has(chain.to_def)) continue;
17863
+ if (taintBarrierDefIds.has(chain.to_def)) continue;
17412
17864
  const targetDef = defById.get(chain.to_def);
17413
17865
  if (!targetDef) continue;
17414
17866
  const sanitizeCheck = checkSanitized(
@@ -17513,6 +17965,9 @@ function maskSanitizerCalls(expr, refs) {
17513
17965
  }
17514
17966
  return out2;
17515
17967
  }
17968
+ function maskLengthOf(expr) {
17969
+ return expr.replace(/\b(?:len|strlen|count)\s*\(\s*[\w$.]+\s*\)/g, " 0 ").replace(/[\w$.]+\s*\.\s*(?:length|size|count)\s*(?:\(\s*\))?(?![\w$(])/g, " 0 ");
17970
+ }
17516
17971
  function blankStringLiterals(expr) {
17517
17972
  return expr.replace(/"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)*'/g, "''");
17518
17973
  }
@@ -17525,12 +17980,15 @@ function identifierOutsideSanitizerCalls(variable, calls, sanitizers, sinkType)
17525
17980
  );
17526
17981
  for (const call of calls) {
17527
17982
  if (refs.some((r) => r.methodName === call.method_name)) continue;
17528
- for (const arg of call.arguments) {
17983
+ const masked = call.arguments.map((arg) => {
17529
17984
  const expr = arg.expression ?? "";
17530
- if (!expr) continue;
17531
- const masked = maskSanitizerCalls(expr, refs);
17532
- if (masked === expr) continue;
17533
- if (re.test(blankStringLiterals(masked))) return true;
17985
+ return { expr, masked: expr ? maskSanitizerCalls(expr, refs) : expr };
17986
+ });
17987
+ if (!masked.some((m) => m.masked !== m.expr)) continue;
17988
+ for (const m of masked) {
17989
+ if (!m.masked) continue;
17990
+ const text = m.masked === m.expr ? maskLengthOf(m.masked) : m.masked;
17991
+ if (re.test(blankStringLiterals(text))) return true;
17534
17992
  }
17535
17993
  }
17536
17994
  return false;