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
@@ -4321,7 +4321,7 @@ function detectLanguage(tree) {
4321
4321
  }
4322
4322
  function extractTypes(tree, cache, language) {
4323
4323
  const effectiveLanguage = language ?? detectLanguage(tree);
4324
- const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
4324
+ const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
4325
4325
  const isPython = effectiveLanguage === "python";
4326
4326
  const isRust = effectiveLanguage === "rust";
4327
4327
  if (effectiveLanguage === "go") {
@@ -5707,7 +5707,7 @@ function detectLanguageFromTree(tree, cache) {
5707
5707
  function extractCalls(tree, cache, language) {
5708
5708
  const calls = [];
5709
5709
  const detectedLanguage = language ?? detectLanguageFromTree(tree, cache);
5710
- const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
5710
+ const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript" || detectedLanguage === "tsx";
5711
5711
  const isPython = detectedLanguage === "python";
5712
5712
  const isRust = detectedLanguage === "rust";
5713
5713
  if (detectedLanguage === "go") {
@@ -5756,8 +5756,137 @@ function extractJavaScriptCalls(tree, cache) {
5756
5756
  calls.push(callInfo);
5757
5757
  }
5758
5758
  }
5759
+ const jsxAttributes = getNodesFromCache(tree.rootNode, "jsx_attribute", cache);
5760
+ for (const attr of jsxAttributes) {
5761
+ const callInfo = extractJSXAttributeSink(attr);
5762
+ if (callInfo) {
5763
+ calls.push(callInfo);
5764
+ }
5765
+ }
5766
+ const assignments = getNodesFromCache(tree.rootNode, "assignment_expression", cache);
5767
+ for (const assign of assignments) {
5768
+ const callInfo = extractDomPropertyAssignmentSink(assign);
5769
+ if (callInfo) {
5770
+ calls.push(callInfo);
5771
+ }
5772
+ }
5759
5773
  return calls;
5760
5774
  }
5775
+ var DOM_XSS_ASSIGNMENT_PROPERTIES = /* @__PURE__ */ new Set([
5776
+ "innerHTML",
5777
+ "outerHTML"
5778
+ ]);
5779
+ function extractDomPropertyAssignmentSink(node) {
5780
+ const leftNode = node.childForFieldName("left");
5781
+ const rightNode = node.childForFieldName("right");
5782
+ if (!leftNode || !rightNode) return null;
5783
+ if (leftNode.type !== "member_expression") return null;
5784
+ const propertyNode = leftNode.childForFieldName("property");
5785
+ const objectNode = leftNode.childForFieldName("object");
5786
+ if (!propertyNode) return null;
5787
+ const propertyName = getNodeText(propertyNode);
5788
+ if (!DOM_XSS_ASSIGNMENT_PROPERTIES.has(propertyName)) return null;
5789
+ const receiver = objectNode ? getNodeText(objectNode) : null;
5790
+ const expression = getNodeText(rightNode);
5791
+ const { variable, literal } = analyzeJSArgument(rightNode);
5792
+ const enclosingFunc = findJSEnclosingFunction(node);
5793
+ return {
5794
+ method_name: propertyName,
5795
+ receiver,
5796
+ arguments: [
5797
+ {
5798
+ position: 0,
5799
+ expression,
5800
+ variable,
5801
+ literal
5802
+ }
5803
+ ],
5804
+ location: {
5805
+ line: node.startPosition.row + 1,
5806
+ column: node.startPosition.column
5807
+ },
5808
+ in_method: enclosingFunc,
5809
+ resolved: true,
5810
+ resolution: {
5811
+ status: "resolved",
5812
+ target: `DOM.${propertyName}`
5813
+ }
5814
+ };
5815
+ }
5816
+ function extractJSXAttributeSink(attr) {
5817
+ let nameNode = null;
5818
+ for (let i2 = 0; i2 < attr.childCount; i2++) {
5819
+ const child = attr.child(i2);
5820
+ if (child && child.type === "property_identifier") {
5821
+ nameNode = child;
5822
+ break;
5823
+ }
5824
+ }
5825
+ if (!nameNode) return null;
5826
+ const attrName = getNodeText(nameNode);
5827
+ if (attrName !== "dangerouslySetInnerHTML") return null;
5828
+ let valueExpr = null;
5829
+ for (let i2 = 0; i2 < attr.childCount; i2++) {
5830
+ const child = attr.child(i2);
5831
+ if (child && child.type === "jsx_expression") {
5832
+ valueExpr = child;
5833
+ break;
5834
+ }
5835
+ }
5836
+ if (!valueExpr) return null;
5837
+ let htmlValue = null;
5838
+ for (let i2 = 0; i2 < valueExpr.childCount; i2++) {
5839
+ const inner = valueExpr.child(i2);
5840
+ if (!inner || inner.type !== "object") continue;
5841
+ for (let j = 0; j < inner.childCount; j++) {
5842
+ const pair = inner.child(j);
5843
+ if (!pair || pair.type !== "pair") continue;
5844
+ const keyNode = pair.childForFieldName("key");
5845
+ if (!keyNode) continue;
5846
+ const keyText = getNodeText(keyNode).replace(/^["']|["']$/g, "");
5847
+ if (keyText === "__html") {
5848
+ htmlValue = pair.childForFieldName("value");
5849
+ break;
5850
+ }
5851
+ }
5852
+ if (htmlValue) break;
5853
+ }
5854
+ if (!htmlValue) {
5855
+ for (let i2 = 0; i2 < valueExpr.childCount; i2++) {
5856
+ const inner = valueExpr.child(i2);
5857
+ if (inner && inner.type !== "{" && inner.type !== "}") {
5858
+ htmlValue = inner;
5859
+ break;
5860
+ }
5861
+ }
5862
+ }
5863
+ if (!htmlValue) return null;
5864
+ const expression = getNodeText(htmlValue);
5865
+ const { variable, literal } = analyzeJSArgument(htmlValue);
5866
+ const enclosingFunc = findJSEnclosingFunction(attr);
5867
+ return {
5868
+ method_name: "dangerouslySetInnerHTML",
5869
+ receiver: null,
5870
+ arguments: [
5871
+ {
5872
+ position: 0,
5873
+ expression,
5874
+ variable,
5875
+ literal
5876
+ }
5877
+ ],
5878
+ location: {
5879
+ line: attr.startPosition.row + 1,
5880
+ column: attr.startPosition.column
5881
+ },
5882
+ in_method: enclosingFunc,
5883
+ resolved: true,
5884
+ resolution: {
5885
+ status: "resolved",
5886
+ target: "react.dangerouslySetInnerHTML"
5887
+ }
5888
+ };
5889
+ }
5761
5890
  function buildJSResolutionContext(tree, cache) {
5762
5891
  const context = {
5763
5892
  functionNames: /* @__PURE__ */ new Set(),
@@ -7265,7 +7394,7 @@ function detectLanguage2(tree) {
7265
7394
  }
7266
7395
  function extractImports(tree, language) {
7267
7396
  const effectiveLanguage = language ?? detectLanguage2(tree);
7268
- const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
7397
+ const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
7269
7398
  const isPython = effectiveLanguage === "python";
7270
7399
  const isRust = effectiveLanguage === "rust";
7271
7400
  if (effectiveLanguage === "go") {
@@ -7957,7 +8086,7 @@ function detectLanguage3(tree) {
7957
8086
  }
7958
8087
  function buildCFG(tree, language) {
7959
8088
  const effectiveLanguage = language ?? detectLanguage3(tree);
7960
- const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
8089
+ const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
7961
8090
  const allBlocks = [];
7962
8091
  const allEdges = [];
7963
8092
  let blockIdCounter = 0;
@@ -8532,7 +8661,7 @@ function detectLanguage4(tree) {
8532
8661
  }
8533
8662
  function buildDFG(tree, cache, language) {
8534
8663
  const effectiveLanguage = language ?? detectLanguage4(tree);
8535
- const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript";
8664
+ const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
8536
8665
  if (isJavaScript) {
8537
8666
  return buildJavaScriptDFG(tree, cache);
8538
8667
  }
@@ -9340,7 +9469,18 @@ function buildBashDFG(tree) {
9340
9469
  if (varNameNode) {
9341
9470
  const varName = getNodeText(varNameNode);
9342
9471
  if (varName && !varName.startsWith("?") && !varName.startsWith("#")) {
9343
- const reachingDef = findReachingDef(varName, scopeStack);
9472
+ let reachingDef = findReachingDef(varName, scopeStack);
9473
+ if (reachingDef === null && !positionalParams.includes(varName)) {
9474
+ const def = {
9475
+ id: defIdCounter++,
9476
+ variable: varName,
9477
+ line: 0,
9478
+ kind: "param"
9479
+ };
9480
+ defs.push(def);
9481
+ scopeStack[0].set(varName, def.id);
9482
+ reachingDef = def.id;
9483
+ }
9344
9484
  uses.push({
9345
9485
  id: useIdCounter++,
9346
9486
  variable: varName,
@@ -9353,7 +9493,18 @@ function buildBashDFG(tree) {
9353
9493
  const varNameNode = node.namedChildCount > 0 ? node.namedChild(0) : null;
9354
9494
  if (varNameNode && varNameNode.type === "variable_name") {
9355
9495
  const varName = getNodeText(varNameNode);
9356
- const reachingDef = findReachingDef(varName, scopeStack);
9496
+ let reachingDef = findReachingDef(varName, scopeStack);
9497
+ if (reachingDef === null && !positionalParams.includes(varName)) {
9498
+ const def = {
9499
+ id: defIdCounter++,
9500
+ variable: varName,
9501
+ line: 0,
9502
+ kind: "param"
9503
+ };
9504
+ defs.push(def);
9505
+ scopeStack[0].set(varName, def.id);
9506
+ reachingDef = def.id;
9507
+ }
9357
9508
  uses.push({
9358
9509
  id: useIdCounter++,
9359
9510
  variable: varName,
@@ -10102,8 +10253,12 @@ var DEFAULT_SOURCES = [
10102
10253
  { method: "get", class: "cookies", type: "http_cookie", severity: "high", return_tainted: true },
10103
10254
  { property: "json", object: "request", type: "http_body", severity: "high", property_tainted: true },
10104
10255
  { property: "data", object: "request", type: "http_body", severity: "high", property_tainted: true },
10256
+ { property: "stream", object: "request", type: "http_body", severity: "high", property_tainted: true },
10105
10257
  { property: "path", object: "request", type: "http_path", severity: "medium", property_tainted: true },
10106
10258
  { property: "query_string", object: "request", type: "http_query", severity: "high", property_tainted: true },
10259
+ // Flask request.get_data() — raw request bytes (method form, parallel to request.data property)
10260
+ { method: "get_data", class: "request", type: "http_body", severity: "high", return_tainted: true },
10261
+ { method: "get_json", class: "request", type: "http_body", severity: "high", return_tainted: true },
10107
10262
  // Django request object
10108
10263
  { method: "get", class: "GET", type: "http_param", severity: "high", return_tainted: true },
10109
10264
  { method: "get", class: "POST", type: "http_param", severity: "high", return_tainted: true },
@@ -10315,14 +10470,19 @@ var DEFAULT_SINKS = [
10315
10470
  { method: "setExecutable", class: "ExecTask", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10316
10471
  { method: "setCommand", class: "ExecTask", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10317
10472
  { method: "execute", class: "Java", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10318
- // Shell/Bash utilities
10319
- { method: "bash", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10320
- { method: "shell", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10321
- { method: "sh", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10322
- { method: "spawn", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10323
- { method: "fork", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10324
- { method: "popen", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10325
- { method: "system", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10473
+ // Shell/Bash utilities — these are method-call sinks in host languages
10474
+ // (Java Runtime/ProcessBuilder, JS child_process spawn/exec, Python subprocess, etc.).
10475
+ // When the analyzed file IS a bash/shell script, the bash plugin's per-flag entries
10476
+ // (argPositions: [1] for `bash -c <cmd>`) MUST win. Restrict these generic entries
10477
+ // to non-shell languages so they don't collide on the dedup key
10478
+ // `${location}:${call.location.line}:${pattern.cwe}`.
10479
+ { method: "bash", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10480
+ { method: "shell", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10481
+ { method: "sh", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10482
+ { method: "spawn", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10483
+ { method: "fork", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10484
+ { method: "popen", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10485
+ { method: "system", languages: ["java", "javascript", "typescript", "python", "go", "rust"], type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
10326
10486
  // Apache Commons Exec
10327
10487
  // Note: bare class 'Executor' removed (see comment above) — DefaultExecutor matched explicitly.
10328
10488
  { method: "setCommandline", class: "DefaultExecutor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
@@ -10412,6 +10572,12 @@ var DEFAULT_SINKS = [
10412
10572
  { method: "unzip", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
10413
10573
  { method: "extract", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
10414
10574
  { method: "extractAll", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
10575
+ // Python zipfile/tarfile use lowercase extractall (PEP 8 naming)
10576
+ { method: "extractall", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0], languages: ["python"] },
10577
+ // Python zipfile.ZipFile(path) — tainted archive path enables Zip-Slip via malicious archive
10578
+ { method: "ZipFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["python"] },
10579
+ // Flask send_from_directory: untrusted filename can escape directory via ../
10580
+ { method: "send_from_directory", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [1], languages: ["python"] },
10415
10581
  { method: "unjar", type: "path_traversal", cwe: "CWE-22", severity: "critical", arg_positions: [0, 1] },
10416
10582
  // Additional file constructors — BufferedReader(Reader) is NOT a path traversal sink; it wraps a Reader, not a file path
10417
10583
  { method: "PrintWriter", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
@@ -11496,16 +11662,42 @@ var DEFAULT_SINKS = [
11496
11662
  // value position so a tainted variable is detected.
11497
11663
  { method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11498
11664
  { method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11499
- // Mass-assignment (CWE-915) — Sprint 6, #86.
11500
- // JS Object.assign(target, ...sources) — sources are arg 1..N, and if any
11501
- // source is request-tainted, every key gets written onto the target. We
11502
- // flag the source positions; the analyzer only needs one tainted to fire.
11503
- { method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11504
- // Lodash bulk-merge helpers behave identically.
11505
- { method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11506
- { method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11665
+ // Mass-assignment (CWE-915 / CWE-1321) — Sprint 6, #86; cognium-dev #68 Sprint 10.
11666
+ // JS Object.assign(target, ...sources), `_.merge`, `_.extend`, `$.extend`,
11667
+ // `Object.defineProperty` when fed an attacker-controlled bag, they write
11668
+ // arbitrary keys onto the target (or, for `__proto__`/`constructor.prototype`,
11669
+ // pollute the prototype chain). The CWE is CWE-1321 (Prototype Pollution),
11670
+ // which subsumes mass assignment for JS sinks operating on plain Objects.
11671
+ // We keep the existing `mass_assignment` SinkType so consumers route the
11672
+ // findings the same way; only the CWE shifts to flag prototype-pollution.
11673
+ { method: "assign", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11674
+ { method: "defineProperty", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2], languages: ["javascript", "typescript"] },
11675
+ { method: "defineProperties", class: "Object", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1], languages: ["javascript", "typescript"] },
11676
+ // Lodash bulk-merge helpers behave identically. `_.merge` and `lodash.merge`
11677
+ // are aliases — match both receivers.
11678
+ { method: "merge", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11679
+ { method: "merge", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11680
+ { method: "extend", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11681
+ { method: "extend", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11682
+ { method: "defaultsDeep", class: "_", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11683
+ { method: "defaultsDeep", class: "lodash", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11507
11684
  // jQuery $.extend(target, source) (legacy).
11508
- { method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-915", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] }
11685
+ { method: "extend", class: "$", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11686
+ { method: "extend", class: "jQuery", type: "mass_assignment", cwe: "CWE-1321", severity: "high", arg_positions: [1, 2, 3], languages: ["javascript", "typescript"] },
11687
+ // DOM-XSS via property assignment (CWE-79) — cognium-dev #68 Sprint 10.
11688
+ // `el.innerHTML = tainted` / `el.outerHTML = tainted`. The JS call extractor
11689
+ // emits a synthetic CallInfo with method=`innerHTML`/`outerHTML` for each
11690
+ // matching assignment_expression. These classless entries catch them.
11691
+ { method: "innerHTML", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11692
+ { method: "outerHTML", type: "xss", cwe: "CWE-79", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11693
+ // node-serialize.unserialize (CWE-502) — cognium-dev #68 Sprint 10.
11694
+ // The node-serialize package evaluates `_$$ND_FUNC$$_` IIFE payloads on
11695
+ // decode, turning untrusted input into RCE. Match both receiver-bound
11696
+ // calls (`serialize.unserialize(x)`) and destructured imports
11697
+ // (`const { unserialize } = require('node-serialize')`).
11698
+ { method: "unserialize", class: "serialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11699
+ { method: "unserialize", class: "node-serialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] },
11700
+ { method: "unserialize", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0], languages: ["javascript", "typescript"] }
11509
11701
  ];
11510
11702
  var DEFAULT_SANITIZERS = [
11511
11703
  // SQL Injection - proper parameter binding sanitizes input
@@ -11780,7 +11972,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11780
11972
  location: formatCallLocation(call),
11781
11973
  severity: pattern.severity,
11782
11974
  line: call.location.line,
11783
- confidence: 1
11975
+ confidence: 1,
11976
+ in_method: call.in_method ?? void 0
11784
11977
  });
11785
11978
  }
11786
11979
  }
@@ -11797,7 +11990,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11797
11990
  location: `@${pattern.annotation} ${param.name} in ${method.name}`,
11798
11991
  severity: pattern.severity,
11799
11992
  line: paramLine,
11800
- confidence: 1
11993
+ confidence: 1,
11994
+ in_method: method.name
11801
11995
  });
11802
11996
  }
11803
11997
  }
@@ -11817,7 +12011,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11817
12011
  location: `@${pattern.method_annotation} ${param.name} in ${method.name}`,
11818
12012
  severity: pattern.severity,
11819
12013
  line: paramLine,
11820
- confidence: 1
12014
+ confidence: 1,
12015
+ in_method: method.name
11821
12016
  });
11822
12017
  }
11823
12018
  }
@@ -11844,7 +12039,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11844
12039
  severity: "high",
11845
12040
  line: paramLine,
11846
12041
  confidence: 1,
11847
- variable: param.name
12042
+ variable: param.name,
12043
+ in_method: method.name
11848
12044
  });
11849
12045
  }
11850
12046
  }
@@ -11863,8 +12059,9 @@ function findSources(calls, types, patterns, sourceLines, language) {
11863
12059
  location: `${param.type || "any"} ${param.name} in ${method.name}`,
11864
12060
  severity: "medium",
11865
12061
  line: paramLine,
11866
- confidence: param.type ? 0.7 : 0.5
12062
+ confidence: param.type ? 0.7 : 0.5,
11867
12063
  // Lower confidence for untyped params
12064
+ in_method: method.name
11868
12065
  });
11869
12066
  }
11870
12067
  }
@@ -11884,7 +12081,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11884
12081
  location: `${arg.expression} in ${call.in_method || "anonymous"}`,
11885
12082
  severity: "high",
11886
12083
  line: call.location.line,
11887
- confidence: 1
12084
+ confidence: 1,
12085
+ in_method: call.in_method ?? void 0
11888
12086
  });
11889
12087
  }
11890
12088
  }
@@ -11905,7 +12103,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
11905
12103
  location: `${arg.expression} in ${call.in_method || "anonymous"}`,
11906
12104
  severity: "high",
11907
12105
  line: call.location.line,
11908
- confidence: 1
12106
+ confidence: 1,
12107
+ in_method: call.in_method ?? void 0
11909
12108
  });
11910
12109
  }
11911
12110
  break;
@@ -11936,6 +12135,18 @@ function findSources(calls, types, patterns, sourceLines, language) {
11936
12135
  if (m) s.variable = m[1];
11937
12136
  }
11938
12137
  }
12138
+ if (language === "java" && sourceLines) {
12139
+ 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*=(?!=)/;
12140
+ for (const s of result) {
12141
+ if (s.variable && s.variable.length > 0) continue;
12142
+ const lineText = sourceLines[s.line - 1] ?? "";
12143
+ const m = JAVA_ASSIGN_LHS.exec(lineText);
12144
+ if (!m) continue;
12145
+ const rhs = lineText.slice(m[0].length).trimStart();
12146
+ if (/^new\b/.test(rhs)) continue;
12147
+ s.variable = m[1];
12148
+ }
12149
+ }
11939
12150
  return result;
11940
12151
  }
11941
12152
  function isInterproceduralTaintableType(typeName) {
@@ -12112,7 +12323,12 @@ function matchesSourcePattern(call, pattern) {
12112
12323
  if (call.receiver_type && call.receiver_type === pattern.class) {
12113
12324
  } else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
12114
12325
  } else if (!call.receiver) {
12115
- return false;
12326
+ const target = call.resolution?.target;
12327
+ const expectedTail = `${pattern.class}.${pattern.method}`;
12328
+ if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
12329
+ } else {
12330
+ return false;
12331
+ }
12116
12332
  } else if (!receiverMightBeClass(call.receiver, pattern.class)) {
12117
12333
  return false;
12118
12334
  }
@@ -12379,7 +12595,12 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
12379
12595
  }
12380
12596
  return false;
12381
12597
  } else if (!call.receiver && !call.receiver_type) {
12382
- return false;
12598
+ const target = call.resolution?.target;
12599
+ const expectedTail = `${pattern.class}.${pattern.method}`;
12600
+ if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
12601
+ } else {
12602
+ return false;
12603
+ }
12383
12604
  }
12384
12605
  }
12385
12606
  if (!pattern.class && call.receiver) {
@@ -13210,6 +13431,7 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
13210
13431
  for (const def of defsNextLine) {
13211
13432
  const callsOnSourceLine = callsByLine.get(source.line) ?? [];
13212
13433
  if (callsOnSourceLine.length > 0) {
13434
+ if (source.variable && def.variable !== source.variable) continue;
13213
13435
  tainted.push({
13214
13436
  variable: def.variable,
13215
13437
  defId: def.id,
@@ -13221,6 +13443,21 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
13221
13443
  });
13222
13444
  }
13223
13445
  }
13446
+ if (source.variable) {
13447
+ const paramDefs = defsByLine.get(0) ?? [];
13448
+ for (const def of paramDefs) {
13449
+ if (def.kind === "param" && def.variable === source.variable) {
13450
+ tainted.push({
13451
+ variable: def.variable,
13452
+ defId: def.id,
13453
+ line: def.line,
13454
+ sourceType: source.type,
13455
+ sourceLine: source.line,
13456
+ confidence: source.confidence
13457
+ });
13458
+ }
13459
+ }
13460
+ }
13224
13461
  }
13225
13462
  return tainted;
13226
13463
  }