circle-ir 3.58.0 → 3.62.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 (65) 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 +283 -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.js +2 -1
  21. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  22. package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
  23. package/dist/analysis/passes/weak-random-pass.js +2 -1
  24. package/dist/analysis/passes/weak-random-pass.js.map +1 -1
  25. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  26. package/dist/analysis/taint-matcher.js +29 -7
  27. package/dist/analysis/taint-matcher.js.map +1 -1
  28. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  29. package/dist/analysis/taint-propagation.js +20 -0
  30. package/dist/analysis/taint-propagation.js.map +1 -1
  31. package/dist/analyzer.d.ts.map +1 -1
  32. package/dist/analyzer.js +19 -2
  33. package/dist/analyzer.js.map +1 -1
  34. package/dist/browser/circle-ir.js +512 -51
  35. package/dist/core/circle-ir-core.cjs +243 -26
  36. package/dist/core/circle-ir-core.js +243 -26
  37. package/dist/core/extractors/calls.js +181 -1
  38. package/dist/core/extractors/calls.js.map +1 -1
  39. package/dist/core/extractors/cfg.js +1 -1
  40. package/dist/core/extractors/cfg.js.map +1 -1
  41. package/dist/core/extractors/dfg.js +29 -3
  42. package/dist/core/extractors/dfg.js.map +1 -1
  43. package/dist/core/extractors/imports.js +1 -1
  44. package/dist/core/extractors/imports.js.map +1 -1
  45. package/dist/core/extractors/runtime-registrations.js +1 -1
  46. package/dist/core/extractors/runtime-registrations.js.map +1 -1
  47. package/dist/core/extractors/types.js +1 -1
  48. package/dist/core/extractors/types.js.map +1 -1
  49. package/dist/core/parser.d.ts +1 -1
  50. package/dist/core/parser.d.ts.map +1 -1
  51. package/dist/graph/scope-graph.d.ts.map +1 -1
  52. package/dist/graph/scope-graph.js +1 -0
  53. package/dist/graph/scope-graph.js.map +1 -1
  54. package/dist/languages/plugins/bash.d.ts.map +1 -1
  55. package/dist/languages/plugins/bash.js +17 -0
  56. package/dist/languages/plugins/bash.js.map +1 -1
  57. package/dist/languages/registry.d.ts.map +1 -1
  58. package/dist/languages/registry.js +6 -0
  59. package/dist/languages/registry.js.map +1 -1
  60. package/dist/languages/types.d.ts +1 -1
  61. package/dist/languages/types.d.ts.map +1 -1
  62. package/dist/types/index.d.ts +1 -1
  63. package/dist/types/index.d.ts.map +1 -1
  64. package/dist/wasm/tree-sitter-tsx.wasm +0 -0
  65. 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
@@ -12112,7 +12304,12 @@ function matchesSourcePattern(call, pattern) {
12112
12304
  if (call.receiver_type && call.receiver_type === pattern.class) {
12113
12305
  } else if (call.receiver_type_fqn && call.receiver_type_fqn.endsWith("." + pattern.class)) {
12114
12306
  } else if (!call.receiver) {
12115
- return false;
12307
+ const target = call.resolution?.target;
12308
+ const expectedTail = `${pattern.class}.${pattern.method}`;
12309
+ if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
12310
+ } else {
12311
+ return false;
12312
+ }
12116
12313
  } else if (!receiverMightBeClass(call.receiver, pattern.class)) {
12117
12314
  return false;
12118
12315
  }
@@ -12379,7 +12576,12 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
12379
12576
  }
12380
12577
  return false;
12381
12578
  } else if (!call.receiver && !call.receiver_type) {
12382
- return false;
12579
+ const target = call.resolution?.target;
12580
+ const expectedTail = `${pattern.class}.${pattern.method}`;
12581
+ if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
12582
+ } else {
12583
+ return false;
12584
+ }
12383
12585
  }
12384
12586
  }
12385
12587
  if (!pattern.class && call.receiver) {
@@ -13221,6 +13423,21 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
13221
13423
  });
13222
13424
  }
13223
13425
  }
13426
+ if (source.variable) {
13427
+ const paramDefs = defsByLine.get(0) ?? [];
13428
+ for (const def of paramDefs) {
13429
+ if (def.kind === "param" && def.variable === source.variable) {
13430
+ tainted.push({
13431
+ variable: def.variable,
13432
+ defId: def.id,
13433
+ line: def.line,
13434
+ sourceType: source.type,
13435
+ sourceLine: source.line,
13436
+ confidence: source.confidence
13437
+ });
13438
+ }
13439
+ }
13440
+ }
13224
13441
  }
13225
13442
  return tainted;
13226
13443
  }
@@ -57,7 +57,7 @@ export function extractCalls(tree, cache, language) {
57
57
  const calls = [];
58
58
  // Use language hint if provided, otherwise detect from tree
59
59
  const detectedLanguage = language ?? detectLanguageFromTree(tree, cache);
60
- const isJavaScript = detectedLanguage === 'javascript' || detectedLanguage === 'typescript';
60
+ const isJavaScript = detectedLanguage === 'javascript' || detectedLanguage === 'typescript' || detectedLanguage === 'tsx';
61
61
  const isPython = detectedLanguage === 'python';
62
62
  const isRust = detectedLanguage === 'rust';
63
63
  if (detectedLanguage === 'go') {
@@ -115,8 +115,188 @@ function extractJavaScriptCalls(tree, cache) {
115
115
  calls.push(callInfo);
116
116
  }
117
117
  }
118
+ // Find JSX attributes that act as XSS sinks (e.g. dangerouslySetInnerHTML).
119
+ // The TSX/JSX grammar represents these as `jsx_attribute` nodes, not
120
+ // `call_expression`. To let the taint matcher reuse the standard method-
121
+ // call sink path, we emit a synthetic CallInfo per matching attribute.
122
+ // (cognium-dev #68 — Phase D.1)
123
+ const jsxAttributes = getNodesFromCache(tree.rootNode, 'jsx_attribute', cache);
124
+ for (const attr of jsxAttributes) {
125
+ const callInfo = extractJSXAttributeSink(attr);
126
+ if (callInfo) {
127
+ calls.push(callInfo);
128
+ }
129
+ }
130
+ // Find DOM property assignments that act as XSS sinks
131
+ // (e.g. `el.innerHTML = userInput`). Same rationale as JSX attributes:
132
+ // emit a synthetic CallInfo so the standard taint matcher path picks them
133
+ // up via property-named sink entries. (cognium-dev #68 — Phase D.3)
134
+ const assignments = getNodesFromCache(tree.rootNode, 'assignment_expression', cache);
135
+ for (const assign of assignments) {
136
+ const callInfo = extractDomPropertyAssignmentSink(assign);
137
+ if (callInfo) {
138
+ calls.push(callInfo);
139
+ }
140
+ }
118
141
  return calls;
119
142
  }
143
+ /**
144
+ * DOM properties whose assignment is an XSS sink. Keeping this list small
145
+ * and explicit so we don't over-flag (the YAML config in
146
+ * `configs/sinks/javascript_dom_xss.yaml` lists more, but most of those are
147
+ * element-conditional and would need DOM-type tracking to flag without FPs).
148
+ */
149
+ const DOM_XSS_ASSIGNMENT_PROPERTIES = new Set([
150
+ 'innerHTML',
151
+ 'outerHTML',
152
+ ]);
153
+ /**
154
+ * Emit a synthetic CallInfo for DOM property assignments that are XSS sinks.
155
+ *
156
+ * Matches `<obj>.<prop> = <expr>` where `<prop>` is in
157
+ * `DOM_XSS_ASSIGNMENT_PROPERTIES`. The synthetic call has method=`<prop>`,
158
+ * receiver=`<obj>` text, single argument=`<expr>`.
159
+ */
160
+ function extractDomPropertyAssignmentSink(node) {
161
+ const leftNode = node.childForFieldName('left');
162
+ const rightNode = node.childForFieldName('right');
163
+ if (!leftNode || !rightNode)
164
+ return null;
165
+ if (leftNode.type !== 'member_expression')
166
+ return null;
167
+ const propertyNode = leftNode.childForFieldName('property');
168
+ const objectNode = leftNode.childForFieldName('object');
169
+ if (!propertyNode)
170
+ return null;
171
+ const propertyName = getNodeText(propertyNode);
172
+ if (!DOM_XSS_ASSIGNMENT_PROPERTIES.has(propertyName))
173
+ return null;
174
+ const receiver = objectNode ? getNodeText(objectNode) : null;
175
+ const expression = getNodeText(rightNode);
176
+ const { variable, literal } = analyzeJSArgument(rightNode);
177
+ const enclosingFunc = findJSEnclosingFunction(node);
178
+ return {
179
+ method_name: propertyName,
180
+ receiver,
181
+ arguments: [
182
+ {
183
+ position: 0,
184
+ expression,
185
+ variable,
186
+ literal,
187
+ },
188
+ ],
189
+ location: {
190
+ line: node.startPosition.row + 1,
191
+ column: node.startPosition.column,
192
+ },
193
+ in_method: enclosingFunc,
194
+ resolved: true,
195
+ resolution: {
196
+ status: 'resolved',
197
+ target: `DOM.${propertyName}`,
198
+ },
199
+ };
200
+ }
201
+ /**
202
+ * Emit a synthetic CallInfo for JSX attributes that are known XSS sinks.
203
+ *
204
+ * Currently handles `dangerouslySetInnerHTML={{ __html: expr }}` on any JSX
205
+ * element. The synthetic call has method `dangerouslySetInnerHTML`, no
206
+ * receiver, and a single argument carrying the `__html` value expression.
207
+ * The taint matcher then catches the call via the standard
208
+ * `dangerouslySetInnerHTML` sink entry in `configs/sinks/nodejs.json`.
209
+ */
210
+ function extractJSXAttributeSink(attr) {
211
+ // `jsx_attribute` has children: property_identifier '=' jsx_expression
212
+ // Get the attribute name (first property_identifier child).
213
+ let nameNode = null;
214
+ for (let i = 0; i < attr.childCount; i++) {
215
+ const child = attr.child(i);
216
+ if (child && child.type === 'property_identifier') {
217
+ nameNode = child;
218
+ break;
219
+ }
220
+ }
221
+ if (!nameNode)
222
+ return null;
223
+ const attrName = getNodeText(nameNode);
224
+ if (attrName !== 'dangerouslySetInnerHTML')
225
+ return null;
226
+ // Find the `jsx_expression` value child (the `{{__html: x}}` part).
227
+ let valueExpr = null;
228
+ for (let i = 0; i < attr.childCount; i++) {
229
+ const child = attr.child(i);
230
+ if (child && child.type === 'jsx_expression') {
231
+ valueExpr = child;
232
+ break;
233
+ }
234
+ }
235
+ if (!valueExpr)
236
+ return null;
237
+ // Inside the jsx_expression, find the inner `object` literal, then the
238
+ // `pair` whose key is `__html`, then that pair's `value`.
239
+ let htmlValue = null;
240
+ for (let i = 0; i < valueExpr.childCount; i++) {
241
+ const inner = valueExpr.child(i);
242
+ if (!inner || inner.type !== 'object')
243
+ continue;
244
+ for (let j = 0; j < inner.childCount; j++) {
245
+ const pair = inner.child(j);
246
+ if (!pair || pair.type !== 'pair')
247
+ continue;
248
+ const keyNode = pair.childForFieldName('key');
249
+ if (!keyNode)
250
+ continue;
251
+ const keyText = getNodeText(keyNode).replace(/^["']|["']$/g, '');
252
+ if (keyText === '__html') {
253
+ htmlValue = pair.childForFieldName('value');
254
+ break;
255
+ }
256
+ }
257
+ if (htmlValue)
258
+ break;
259
+ }
260
+ // If the `__html` field wasn't found (or the prop was passed a spread/var
261
+ // like `{...props}`), fall back to using the entire jsx_expression body
262
+ // so the matcher still sees the data dependency.
263
+ if (!htmlValue) {
264
+ for (let i = 0; i < valueExpr.childCount; i++) {
265
+ const inner = valueExpr.child(i);
266
+ if (inner && inner.type !== '{' && inner.type !== '}') {
267
+ htmlValue = inner;
268
+ break;
269
+ }
270
+ }
271
+ }
272
+ if (!htmlValue)
273
+ return null;
274
+ const expression = getNodeText(htmlValue);
275
+ const { variable, literal } = analyzeJSArgument(htmlValue);
276
+ const enclosingFunc = findJSEnclosingFunction(attr);
277
+ return {
278
+ method_name: 'dangerouslySetInnerHTML',
279
+ receiver: null,
280
+ arguments: [
281
+ {
282
+ position: 0,
283
+ expression,
284
+ variable,
285
+ literal,
286
+ },
287
+ ],
288
+ location: {
289
+ line: attr.startPosition.row + 1,
290
+ column: attr.startPosition.column,
291
+ },
292
+ in_method: enclosingFunc,
293
+ resolved: true,
294
+ resolution: {
295
+ status: 'resolved',
296
+ target: 'react.dangerouslySetInnerHTML',
297
+ },
298
+ };
299
+ }
120
300
  function buildJSResolutionContext(tree, cache) {
121
301
  const context = {
122
302
  functionNames: new Set(),