circle-ir 3.59.0 → 3.64.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 (66) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +58 -17
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/html/html-merge.d.ts.map +1 -1
  5. package/dist/analysis/html/html-merge.js +10 -0
  6. package/dist/analysis/html/html-merge.js.map +1 -1
  7. package/dist/analysis/interprocedural.d.ts.map +1 -1
  8. package/dist/analysis/interprocedural.js +44 -11
  9. package/dist/analysis/interprocedural.js.map +1 -1
  10. package/dist/analysis/passes/language-sources-pass.d.ts +7 -1
  11. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  12. package/dist/analysis/passes/language-sources-pass.js +112 -15
  13. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  14. package/dist/analysis/passes/missing-public-doc-pass.d.ts.map +1 -1
  15. package/dist/analysis/passes/missing-public-doc-pass.js +2 -1
  16. package/dist/analysis/passes/missing-public-doc-pass.js.map +1 -1
  17. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  18. package/dist/analysis/passes/sink-filter-pass.js +4 -1
  19. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  20. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  21. package/dist/analysis/passes/taint-propagation-pass.js +222 -10
  22. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  23. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  24. package/dist/analysis/passes/weak-random-pass.js +2 -1
  25. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  26. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  27. package/dist/analysis/taint-matcher.js +83 -7
  28. package/dist/analysis/taint-matcher.js.map +1 -1
  29. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  30. package/dist/analysis/taint-propagation.js +32 -0
  31. package/dist/analysis/taint-propagation.js.map +1 -1
  32. package/dist/analyzer.d.ts.map +1 -1
  33. package/dist/analyzer.js +19 -2
  34. package/dist/analyzer.js.map +1 -1
  35. package/dist/browser/circle-ir.js +526 -67
  36. package/dist/core/circle-ir-core.cjs +270 -33
  37. package/dist/core/circle-ir-core.js +270 -33
  38. package/dist/core/extractors/calls.js +181 -1
  39. package/dist/core/extractors/calls.js.map +1 -1
  40. package/dist/core/extractors/cfg.js +1 -1
  41. package/dist/core/extractors/cfg.js.map +1 -1
  42. package/dist/core/extractors/dfg.js +29 -3
  43. package/dist/core/extractors/dfg.js.map +1 -1
  44. package/dist/core/extractors/imports.js +1 -1
  45. package/dist/core/extractors/imports.js.map +1 -1
  46. package/dist/core/extractors/runtime-registrations.js +1 -1
  47. package/dist/core/extractors/runtime-registrations.js.map +1 -1
  48. package/dist/core/extractors/types.js +1 -1
  49. package/dist/core/extractors/types.js.map +1 -1
  50. package/dist/core/parser.d.ts +1 -1
  51. package/dist/core/parser.d.ts.map +1 -1
  52. package/dist/graph/scope-graph.d.ts.map +1 -1
  53. package/dist/graph/scope-graph.js +1 -0
  54. package/dist/graph/scope-graph.js.map +1 -1
  55. package/dist/languages/plugins/bash.d.ts.map +1 -1
  56. package/dist/languages/plugins/bash.js +17 -0
  57. package/dist/languages/plugins/bash.js.map +1 -1
  58. package/dist/languages/registry.d.ts.map +1 -1
  59. package/dist/languages/registry.js +6 -0
  60. package/dist/languages/registry.js.map +1 -1
  61. package/dist/languages/types.d.ts +1 -1
  62. package/dist/languages/types.d.ts.map +1 -1
  63. package/dist/types/index.d.ts +9 -1
  64. package/dist/types/index.d.ts.map +1 -1
  65. package/dist/wasm/tree-sitter-tsx.wasm +0 -0
  66. package/package.json +2 -1
@@ -4387,7 +4387,7 @@ function detectLanguage(tree) {
4387
4387
  }
4388
4388
  function extractTypes(tree, cache, language) {
4389
4389
  const effectiveLanguage = language ?? detectLanguage(tree);
4390
- const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
4390
+ const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
4391
4391
  const isPython = effectiveLanguage === "python";
4392
4392
  const isRust = effectiveLanguage === "rust";
4393
4393
  if (effectiveLanguage === "go") {
@@ -5773,7 +5773,7 @@ function detectLanguageFromTree(tree, cache) {
5773
5773
  function extractCalls(tree, cache, language) {
5774
5774
  const calls = [];
5775
5775
  const detectedLanguage = language ?? detectLanguageFromTree(tree, cache);
5776
- const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
5776
+ const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript" || detectedLanguage === "tsx";
5777
5777
  const isPython = detectedLanguage === "python";
5778
5778
  const isRust = detectedLanguage === "rust";
5779
5779
  if (detectedLanguage === "go") {
@@ -5822,8 +5822,137 @@ function extractJavaScriptCalls(tree, cache) {
5822
5822
  calls.push(callInfo);
5823
5823
  }
5824
5824
  }
5825
+ const jsxAttributes = getNodesFromCache(tree.rootNode, "jsx_attribute", cache);
5826
+ for (const attr of jsxAttributes) {
5827
+ const callInfo = extractJSXAttributeSink(attr);
5828
+ if (callInfo) {
5829
+ calls.push(callInfo);
5830
+ }
5831
+ }
5832
+ const assignments = getNodesFromCache(tree.rootNode, "assignment_expression", cache);
5833
+ for (const assign of assignments) {
5834
+ const callInfo = extractDomPropertyAssignmentSink(assign);
5835
+ if (callInfo) {
5836
+ calls.push(callInfo);
5837
+ }
5838
+ }
5825
5839
  return calls;
5826
5840
  }
5841
+ var DOM_XSS_ASSIGNMENT_PROPERTIES = /* @__PURE__ */ new Set([
5842
+ "innerHTML",
5843
+ "outerHTML"
5844
+ ]);
5845
+ function extractDomPropertyAssignmentSink(node) {
5846
+ const leftNode = node.childForFieldName("left");
5847
+ const rightNode = node.childForFieldName("right");
5848
+ if (!leftNode || !rightNode) return null;
5849
+ if (leftNode.type !== "member_expression") return null;
5850
+ const propertyNode = leftNode.childForFieldName("property");
5851
+ const objectNode = leftNode.childForFieldName("object");
5852
+ if (!propertyNode) return null;
5853
+ const propertyName = getNodeText(propertyNode);
5854
+ if (!DOM_XSS_ASSIGNMENT_PROPERTIES.has(propertyName)) return null;
5855
+ const receiver = objectNode ? getNodeText(objectNode) : null;
5856
+ const expression = getNodeText(rightNode);
5857
+ const { variable, literal } = analyzeJSArgument(rightNode);
5858
+ const enclosingFunc = findJSEnclosingFunction(node);
5859
+ return {
5860
+ method_name: propertyName,
5861
+ receiver,
5862
+ arguments: [
5863
+ {
5864
+ position: 0,
5865
+ expression,
5866
+ variable,
5867
+ literal
5868
+ }
5869
+ ],
5870
+ location: {
5871
+ line: node.startPosition.row + 1,
5872
+ column: node.startPosition.column
5873
+ },
5874
+ in_method: enclosingFunc,
5875
+ resolved: true,
5876
+ resolution: {
5877
+ status: "resolved",
5878
+ target: `DOM.${propertyName}`
5879
+ }
5880
+ };
5881
+ }
5882
+ function extractJSXAttributeSink(attr) {
5883
+ let nameNode = null;
5884
+ for (let i2 = 0; i2 < attr.childCount; i2++) {
5885
+ const child = attr.child(i2);
5886
+ if (child && child.type === "property_identifier") {
5887
+ nameNode = child;
5888
+ break;
5889
+ }
5890
+ }
5891
+ if (!nameNode) return null;
5892
+ const attrName = getNodeText(nameNode);
5893
+ if (attrName !== "dangerouslySetInnerHTML") return null;
5894
+ let valueExpr = null;
5895
+ for (let i2 = 0; i2 < attr.childCount; i2++) {
5896
+ const child = attr.child(i2);
5897
+ if (child && child.type === "jsx_expression") {
5898
+ valueExpr = child;
5899
+ break;
5900
+ }
5901
+ }
5902
+ if (!valueExpr) return null;
5903
+ let htmlValue = null;
5904
+ for (let i2 = 0; i2 < valueExpr.childCount; i2++) {
5905
+ const inner = valueExpr.child(i2);
5906
+ if (!inner || inner.type !== "object") continue;
5907
+ for (let j = 0; j < inner.childCount; j++) {
5908
+ const pair = inner.child(j);
5909
+ if (!pair || pair.type !== "pair") continue;
5910
+ const keyNode = pair.childForFieldName("key");
5911
+ if (!keyNode) continue;
5912
+ const keyText = getNodeText(keyNode).replace(/^["']|["']$/g, "");
5913
+ if (keyText === "__html") {
5914
+ htmlValue = pair.childForFieldName("value");
5915
+ break;
5916
+ }
5917
+ }
5918
+ if (htmlValue) break;
5919
+ }
5920
+ if (!htmlValue) {
5921
+ for (let i2 = 0; i2 < valueExpr.childCount; i2++) {
5922
+ const inner = valueExpr.child(i2);
5923
+ if (inner && inner.type !== "{" && inner.type !== "}") {
5924
+ htmlValue = inner;
5925
+ break;
5926
+ }
5927
+ }
5928
+ }
5929
+ if (!htmlValue) return null;
5930
+ const expression = getNodeText(htmlValue);
5931
+ const { variable, literal } = analyzeJSArgument(htmlValue);
5932
+ const enclosingFunc = findJSEnclosingFunction(attr);
5933
+ return {
5934
+ method_name: "dangerouslySetInnerHTML",
5935
+ receiver: null,
5936
+ arguments: [
5937
+ {
5938
+ position: 0,
5939
+ expression,
5940
+ variable,
5941
+ literal
5942
+ }
5943
+ ],
5944
+ location: {
5945
+ line: attr.startPosition.row + 1,
5946
+ column: attr.startPosition.column
5947
+ },
5948
+ in_method: enclosingFunc,
5949
+ resolved: true,
5950
+ resolution: {
5951
+ status: "resolved",
5952
+ target: "react.dangerouslySetInnerHTML"
5953
+ }
5954
+ };
5955
+ }
5827
5956
  function buildJSResolutionContext(tree, cache) {
5828
5957
  const context = {
5829
5958
  functionNames: /* @__PURE__ */ new Set(),
@@ -7331,7 +7460,7 @@ function detectLanguage2(tree) {
7331
7460
  }
7332
7461
  function extractImports(tree, language) {
7333
7462
  const effectiveLanguage = language ?? detectLanguage2(tree);
7334
- const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
7463
+ const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
7335
7464
  const isPython = effectiveLanguage === "python";
7336
7465
  const isRust = effectiveLanguage === "rust";
7337
7466
  if (effectiveLanguage === "go") {
@@ -8023,7 +8152,7 @@ function detectLanguage3(tree) {
8023
8152
  }
8024
8153
  function buildCFG(tree, language) {
8025
8154
  const effectiveLanguage = language ?? detectLanguage3(tree);
8026
- const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
8155
+ const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
8027
8156
  const allBlocks = [];
8028
8157
  const allEdges = [];
8029
8158
  let blockIdCounter = 0;
@@ -8598,7 +8727,7 @@ function detectLanguage4(tree) {
8598
8727
  }
8599
8728
  function buildDFG(tree, cache, language) {
8600
8729
  const effectiveLanguage = language ?? detectLanguage4(tree);
8601
- const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
8730
+ const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
8602
8731
  if (isJavaScript) {
8603
8732
  return buildJavaScriptDFG(tree, cache);
8604
8733
  }
@@ -9406,7 +9535,18 @@ function buildBashDFG(tree) {
9406
9535
  if (varNameNode) {
9407
9536
  const varName = getNodeText(varNameNode);
9408
9537
  if (varName && !varName.startsWith("?") && !varName.startsWith("#")) {
9409
- const reachingDef = findReachingDef(varName, scopeStack);
9538
+ let reachingDef = findReachingDef(varName, scopeStack);
9539
+ if (reachingDef === null && !positionalParams.includes(varName)) {
9540
+ const def = {
9541
+ id: defIdCounter++,
9542
+ variable: varName,
9543
+ line: 0,
9544
+ kind: "param"
9545
+ };
9546
+ defs.push(def);
9547
+ scopeStack[0].set(varName, def.id);
9548
+ reachingDef = def.id;
9549
+ }
9410
9550
  uses.push({
9411
9551
  id: useIdCounter++,
9412
9552
  variable: varName,
@@ -9419,7 +9559,18 @@ function buildBashDFG(tree) {
9419
9559
  const varNameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
9420
9560
  if (varNameNode && varNameNode.type === "variable_name") {
9421
9561
  const varName = getNodeText(varNameNode);
9422
- const reachingDef = findReachingDef(varName, scopeStack);
9562
+ let reachingDef = findReachingDef(varName, scopeStack);
9563
+ if (reachingDef === null && !positionalParams.includes(varName)) {
9564
+ const def = {
9565
+ id: defIdCounter++,
9566
+ variable: varName,
9567
+ line: 0,
9568
+ kind: "param"
9569
+ };
9570
+ defs.push(def);
9571
+ scopeStack[0].set(varName, def.id);
9572
+ reachingDef = def.id;
9573
+ }
9423
9574
  uses.push({
9424
9575
  id: useIdCounter++,
9425
9576
  variable: varName,
@@ -10168,8 +10319,12 @@ var DEFAULT_SOURCES = [
10168
10319
  { method: "get", class: "cookies", type: "http_cookie", severity: "high", return_tainted: true },
10169
10320
  { property: "json", object: "request", type: "http_body", severity: "high", property_tainted: true },
10170
10321
  { property: "data", object: "request", type: "http_body", severity: "high", property_tainted: true },
10322
+ { property: "stream", object: "request", type: "http_body", severity: "high", property_tainted: true },
10171
10323
  { property: "path", object: "request", type: "http_path", severity: "medium", property_tainted: true },
10172
10324
  { property: "query_string", object: "request", type: "http_query", severity: "high", property_tainted: true },
10325
+ // Flask request.get_data() — raw request bytes (method form, parallel to request.data property)
10326
+ { method: "get_data", class: "request", type: "http_body", severity: "high", return_tainted: true },
10327
+ { method: "get_json", class: "request", type: "http_body", severity: "high", return_tainted: true },
10173
10328
  // Django request object
10174
10329
  { method: "get", class: "GET", type: "http_param", severity: "high", return_tainted: true },
10175
10330
  { method: "get", class: "POST", type: "http_param", severity: "high", return_tainted: true },
@@ -10381,14 +10536,19 @@ var DEFAULT_SINKS = [
10381
10536
  { method: "setExecutable", class: "ExecTask", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10382
10537
  { method: "setCommand", class: "ExecTask", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10383
10538
  { method: "execute", class: "Java", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10384
- // Shell/Bash utilities
10385
- { method: "bash", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10386
- { method: "shell", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10387
- { method: "sh", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10388
- { method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10389
- { method: "fork", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10390
- { method: "popen", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10391
- { method: "system", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10539
+ // Shell/Bash utilities — these are method-call sinks in host languages
10540
+ // (Java Runtime/ProcessBuilder, JS child_process spawn/exec, Python subprocess, etc.).
10541
+ // When the analyzed file IS a bash/shell script, the bash plugin's per-flag entries
10542
+ // (argPositions: [1] for `bash -c <cmd>`) MUST win. Restrict these generic entries
10543
+ // to non-shell languages so they don't collide on the dedup key
10544
+ // `${location}:${call.location.line}:${pattern.cwe}`.
10545
+ { method: "bash", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10546
+ { method: "shell", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10547
+ { method: "sh", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10548
+ { method: "spawn", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10549
+ { method: "fork", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10550
+ { method: "popen", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10551
+ { method: "system", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10392
10552
  // Apache Commons Exec
10393
10553
  // Note: bare class 'Executor' removed (see comment above) — DefaultExecutor matched explicitly.
10394
10554
  { method: "setCommandline", class: "DefaultExecutor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
@@ -10478,6 +10638,12 @@ var DEFAULT_SINKS = [
10478
10638
  { method: "unzip", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
10479
10639
  { method: "extract", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
10480
10640
  { method: "extractAll", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
10641
+ // Python zipfile/tarfile use lowercase extractall (PEP 8 naming)
10642
+ { method: "extractall", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0], languages: ["python"] },
10643
+ // Python zipfile.ZipFile(path) — tainted archive path enables Zip-Slip via malicious archive
10644
+ { method: "ZipFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
10645
+ // Flask send_from_directory: untrusted filename can escape directory via ../
10646
+ { method: "send_from_directory", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [1], languages: ["python"] },
10481
10647
  { method: "unjar", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
10482
10648
  // Additional file constructors — BufferedReader(Reader) is NOT a path traversal sink; it wraps a Reader, not a file path
10483
10649
  { method: "PrintWriter", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
@@ -11562,16 +11728,42 @@ var DEFAULT_SINKS = [
11562
11728
  // value position so a tainted variable is detected.
11563
11729
  { method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11564
11730
  { method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11565
- // Mass-assignment (CWE-915) — Sprint 6, #86.
11566
- // JS Object.assign(target, ...sources) — sources are arg 1..N, and if any
11567
- // source is request-tainted, every key gets written onto the target. We
11568
- // flag the source positions; the analyzer only needs one tainted to fire.
11569
- { method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11570
- // Lodash bulk-merge helpers behave identically.
11571
- { method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11572
- { method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11731
+ // Mass-assignment (CWE-915 / CWE-1321) — Sprint 6, #86; cognium-dev #68 Sprint 10.
11732
+ // JS Object.assign(target, ...sources), `_.merge`, `_.extend`, `$.extend`,
11733
+ // `Object.defineProperty` when fed an attacker-controlled bag, they write
11734
+ // arbitrary keys onto the target (or, for `__proto__`/`constructor.prototype`,
11735
+ // pollute the prototype chain). The CWE is CWE-1321 (Prototype Pollution),
11736
+ // which subsumes mass assignment for JS sinks operating on plain Objects.
11737
+ // We keep the existing `mass_assignment` SinkType so consumers route the
11738
+ // findings the same way; only the CWE shifts to flag prototype-pollution.
11739
+ { method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11740
+ { method: "defineProperty", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2], languages: ["javascript", "typescript"] },
11741
+ { method: "defineProperties", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1], languages: ["javascript", "typescript"] },
11742
+ // Lodash bulk-merge helpers behave identically. `_.merge` and `lodash.merge`
11743
+ // are aliases — match both receivers.
11744
+ { method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11745
+ { method: "merge", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11746
+ { method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11747
+ { method: "extend", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11748
+ { method: "defaultsDeep", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11749
+ { method: "defaultsDeep", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11573
11750
  // jQuery $.extend(target, source) (legacy).
11574
- { method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] }
11751
+ { method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11752
+ { method: "extend", class: "jQuery", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11753
+ // DOM-XSS via property assignment (CWE-79) — cognium-dev #68 Sprint 10.
11754
+ // `el.innerHTML = tainted` / `el.outerHTML = tainted`. The JS call extractor
11755
+ // emits a synthetic CallInfo with method=`innerHTML`/`outerHTML` for each
11756
+ // matching assignment_expression. These classless entries catch them.
11757
+ { method: "innerHTML", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11758
+ { method: "outerHTML", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11759
+ // node-serialize.unserialize (CWE-502) — cognium-dev #68 Sprint 10.
11760
+ // The node-serialize package evaluates `_$$ND_FUNC$$_` IIFE payloads on
11761
+ // decode, turning untrusted input into RCE. Match both receiver-bound
11762
+ // calls (`serialize.unserialize(x)`) and destructured imports
11763
+ // (`const { unserialize } = require('node-serialize')`).
11764
+ { method: "unserialize", class: "serialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11765
+ { method: "unserialize", class: "node-serialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11766
+ { method: "unserialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] }
11575
11767
  ];
11576
11768
  var DEFAULT_SANITIZERS = [
11577
11769
  // SQL Injection - proper parameter binding sanitizes input
@@ -11846,7 +12038,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11846
12038
  location: formatCallLocation(call),
11847
12039
  severity: pattern.severity,
11848
12040
  line: call.location.line,
11849
- confidence: 1
12041
+ confidence: 1,
12042
+ in_method: call.in_method ?? void 0
11850
12043
  });
11851
12044
  }
11852
12045
  }
@@ -11863,7 +12056,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11863
12056
  location: `@${pattern.annotation} ${param.name} in ${method.name}`,
11864
12057
  severity: pattern.severity,
11865
12058
  line: paramLine,
11866
- confidence: 1
12059
+ confidence: 1,
12060
+ in_method: method.name
11867
12061
  });
11868
12062
  }
11869
12063
  }
@@ -11883,7 +12077,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11883
12077
  location: `@${pattern.method_annotation} ${param.name} in ${method.name}`,
11884
12078
  severity: pattern.severity,
11885
12079
  line: paramLine,
11886
- confidence: 1
12080
+ confidence: 1,
12081
+ in_method: method.name
11887
12082
  });
11888
12083
  }
11889
12084
  }
@@ -11910,7 +12105,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11910
12105
  severity: "high",
11911
12106
  line: paramLine,
11912
12107
  confidence: 1,
11913
- variable: param.name
12108
+ variable: param.name,
12109
+ in_method: method.name
11914
12110
  });
11915
12111
  }
11916
12112
  }
@@ -11929,8 +12125,9 @@ function findSources(calls, types, patterns, sourceLines, language) {
11929
12125
  location: `${param.type || "any"} ${param.name} in ${method.name}`,
11930
12126
  severity: "medium",
11931
12127
  line: paramLine,
11932
- confidence: param.type ? 0.7 : 0.5
12128
+ confidence: param.type ? 0.7 : 0.5,
11933
12129
  // Lower confidence for untyped params
12130
+ in_method: method.name
11934
12131
  });
11935
12132
  }
11936
12133
  }
@@ -11950,7 +12147,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11950
12147
  location: `${arg.expression} in ${call.in_method || "anonymous"}`,
11951
12148
  severity: "high",
11952
12149
  line: call.location.line,
11953
- confidence: 1
12150
+ confidence: 1,
12151
+ in_method: call.in_method ?? void 0
11954
12152
  });
11955
12153
  }
11956
12154
  }
@@ -11971,7 +12169,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11971
12169
  location: `${arg.expression} in ${call.in_method || "anonymous"}`,
11972
12170
  severity: "high",
11973
12171
  line: call.location.line,
11974
- confidence: 1
12172
+ confidence: 1,
12173
+ in_method: call.in_method ?? void 0
11975
12174
  });
11976
12175
  }
11977
12176
  break;
@@ -12002,6 +12201,18 @@ function findSources(calls, types, patterns, sourceLines, language) {
12002
12201
  if (m) s.variable = m[1];
12003
12202
  }
12004
12203
  }
12204
+ if (language === "java" && sourceLines) {
12205
+ const JAVA_ASSIGN_LHS = /^\s*(?:(?:final|public|private|protected|static|synchronized|volatile|transient)\s+)*(?:[A-Za-z_][\w.]*(?:\s*<[^=]*>)?(?:\s*\[\s*\])*\s+)?([A-Za-z_]\w*)\s*=(?!=)/;
12206
+ for (const s of result) {
12207
+ if (s.variable && s.variable.length > 0) continue;
12208
+ const lineText = sourceLines[s.line - 1] ?? "";
12209
+ const m = JAVA_ASSIGN_LHS.exec(lineText);
12210
+ if (!m) continue;
12211
+ const rhs = lineText.slice(m[0].length).trimStart();
12212
+ if (/^new\b/.test(rhs)) continue;
12213
+ s.variable = m[1];
12214
+ }
12215
+ }
12005
12216
  return result;
12006
12217
  }
12007
12218
  function isInterproceduralTaintableType(typeName) {
@@ -12178,7 +12389,12 @@ function matchesSourcePattern(call, pattern) {
12178
12389
  if (call.receiver_type && call.receiver_type === pattern.class) {
12179
12390
  } else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
12180
12391
  } else if (!call.receiver) {
12181
- return false;
12392
+ const target = call.resolution?.target;
12393
+ const expectedTail = `${pattern.class}.${pattern.method}`;
12394
+ if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
12395
+ } else {
12396
+ return false;
12397
+ }
12182
12398
  } else if (!receiverMightBeClass(call.receiver, pattern.class)) {
12183
12399
  return false;
12184
12400
  }
@@ -12445,7 +12661,12 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
12445
12661
  }
12446
12662
  return false;
12447
12663
  } else if (!call.receiver && !call.receiver_type) {
12448
- return false;
12664
+ const target = call.resolution?.target;
12665
+ const expectedTail = `${pattern.class}.${pattern.method}`;
12666
+ if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
12667
+ } else {
12668
+ return false;
12669
+ }
12449
12670
  }
12450
12671
  }
12451
12672
  if (!pattern.class && call.receiver) {
@@ -13276,6 +13497,7 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
13276
13497
  for (const def of defsNextLine) {
13277
13498
  const callsOnSourceLine = callsByLine.get(source.line) ?? [];
13278
13499
  if (callsOnSourceLine.length > 0) {
13500
+ if (source.variable && def.variable !== source.variable) continue;
13279
13501
  tainted.push({
13280
13502
  variable: def.variable,
13281
13503
  defId: def.id,
@@ -13287,6 +13509,21 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
13287
13509
  });
13288
13510
  }
13289
13511
  }
13512
+ if (source.variable) {
13513
+ const paramDefs = defsByLine.get(0) ?? [];
13514
+ for (const def of paramDefs) {
13515
+ if (def.kind === "param" && def.variable === source.variable) {
13516
+ tainted.push({
13517
+ variable: def.variable,
13518
+ defId: def.id,
13519
+ line: def.line,
13520
+ sourceType: source.type,
13521
+ sourceLine: source.line,
13522
+ confidence: source.confidence
13523
+ });
13524
+ }
13525
+ }
13526
+ }
13290
13527
  }
13291
13528
  return tainted;
13292
13529
  }