circle-ir 4.7.2 → 4.8.2

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.
@@ -5980,13 +5980,21 @@ function extractCSharpCalls(tree, cache) {
5980
5980
  const left = asn.childForFieldName("left");
5981
5981
  if (left?.type !== "member_access_expression") continue;
5982
5982
  const nameNode = left.childForFieldName("name");
5983
- if (!nameNode || getNodeText(nameNode) !== "CommandText") continue;
5983
+ if (!nameNode) continue;
5984
+ const propName = getNodeText(nameNode);
5985
+ const exprNode = left.childForFieldName("expression");
5986
+ if (propName === "Filter") {
5987
+ const recv = exprNode ? getNodeText(exprNode) : null;
5988
+ const recvType = recv ? typeMap.get(recv) : void 0;
5989
+ if (recvType !== "DirectorySearcher") continue;
5990
+ } else if (propName !== "CommandText") {
5991
+ continue;
5992
+ }
5984
5993
  const right = asn.childForFieldName("right");
5985
5994
  if (!right) continue;
5986
5995
  const rhsText = getNodeText(right);
5987
- const exprNode = left.childForFieldName("expression");
5988
5996
  calls.push({
5989
- method_name: "CommandText",
5997
+ method_name: propName,
5990
5998
  receiver: exprNode ? getNodeText(exprNode) : null,
5991
5999
  receiver_type: null,
5992
6000
  receiver_type_fqn: null,
@@ -8552,6 +8560,9 @@ function buildCFG(tree, language, cache) {
8552
8560
  if (effectiveLanguage === "go") {
8553
8561
  return buildGoCFG(tree, blockIdCounter, cache);
8554
8562
  }
8563
+ if (effectiveLanguage === "csharp") {
8564
+ return buildCSharpCFG(tree, blockIdCounter, cache);
8565
+ }
8555
8566
  if (isJavaScript) {
8556
8567
  const functions = [
8557
8568
  ...getNodesFromCache(tree.rootNode, "function_declaration", cache),
@@ -8564,7 +8575,7 @@ function buildCFG(tree, language, cache) {
8564
8575
  const body2 = func2.childForFieldName("body");
8565
8576
  if (!body2) continue;
8566
8577
  if (body2.type === "statement_block") {
8567
- const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, true);
8578
+ const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, "js");
8568
8579
  allBlocks.push(...blocks);
8569
8580
  allEdges.push(...edges);
8570
8581
  blockIdCounter = nextId;
@@ -8586,7 +8597,7 @@ function buildCFG(tree, language, cache) {
8586
8597
  for (const method of methods) {
8587
8598
  const body2 = method.childForFieldName("body");
8588
8599
  if (!body2) continue;
8589
- const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, false);
8600
+ const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, "java");
8590
8601
  allBlocks.push(...blocks);
8591
8602
  allEdges.push(...edges);
8592
8603
  blockIdCounter = nextId;
@@ -8594,7 +8605,28 @@ function buildCFG(tree, language, cache) {
8594
8605
  }
8595
8606
  return { blocks: allBlocks, edges: allEdges };
8596
8607
  }
8597
- function buildMethodCFG(body2, startId, isJavaScript) {
8608
+ function buildCSharpCFG(tree, blockIdCounter, cache) {
8609
+ const allBlocks = [];
8610
+ const allEdges = [];
8611
+ const containers = [
8612
+ ...getNodesFromCache(tree.rootNode, "method_declaration", cache),
8613
+ ...getNodesFromCache(tree.rootNode, "constructor_declaration", cache),
8614
+ ...getNodesFromCache(tree.rootNode, "destructor_declaration", cache),
8615
+ ...getNodesFromCache(tree.rootNode, "operator_declaration", cache),
8616
+ ...getNodesFromCache(tree.rootNode, "local_function_statement", cache),
8617
+ ...getNodesFromCache(tree.rootNode, "accessor_declaration", cache)
8618
+ ];
8619
+ for (const container of containers) {
8620
+ const body2 = container.childForFieldName("body");
8621
+ if (!body2 || body2.type !== "block") continue;
8622
+ const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, "csharp");
8623
+ allBlocks.push(...blocks);
8624
+ allEdges.push(...edges);
8625
+ blockIdCounter = nextId;
8626
+ }
8627
+ return { blocks: allBlocks, edges: allEdges };
8628
+ }
8629
+ function buildMethodCFG(body2, startId, dialect) {
8598
8630
  const blocks = [];
8599
8631
  const edges = [];
8600
8632
  let currentId = startId;
@@ -8605,7 +8637,7 @@ function buildMethodCFG(body2, startId, isJavaScript) {
8605
8637
  end_line: body2.startPosition.row + 1
8606
8638
  };
8607
8639
  blocks.push(entryBlock);
8608
- const result = processStatements(body2, currentId, blocks, edges, isJavaScript);
8640
+ const result = processStatements(body2, currentId, blocks, edges, dialect);
8609
8641
  currentId = result.nextId;
8610
8642
  if (result.entryId !== -1) {
8611
8643
  edges.push({
@@ -8637,15 +8669,15 @@ function buildMethodCFG(body2, startId, isJavaScript) {
8637
8669
  }
8638
8670
  return { blocks, edges, nextId: currentId };
8639
8671
  }
8640
- function processStatements(container, startId, blocks, edges, isJavaScript) {
8672
+ function processStatements(container, startId, blocks, edges, dialect) {
8641
8673
  let currentId = startId;
8642
8674
  let firstBlockId = -1;
8643
8675
  let lastExitIds = [];
8644
8676
  for (let i2 = 0; i2 < container.childCount; i2++) {
8645
8677
  const stmt = container.child(i2);
8646
8678
  if (!stmt) continue;
8647
- if (!isStatement(stmt, isJavaScript)) continue;
8648
- const result = processStatement(stmt, currentId, blocks, edges, isJavaScript);
8679
+ if (!isStatement(stmt, dialect)) continue;
8680
+ const result = processStatement(stmt, currentId, blocks, edges, dialect);
8649
8681
  currentId = result.nextId;
8650
8682
  if (firstBlockId === -1) {
8651
8683
  firstBlockId = result.entryId;
@@ -8666,31 +8698,49 @@ function processStatements(container, startId, blocks, edges, isJavaScript) {
8666
8698
  nextId: currentId
8667
8699
  };
8668
8700
  }
8669
- function processStatement(stmt, startId, blocks, edges, isJavaScript) {
8701
+ function processStatement(stmt, startId, blocks, edges, dialect) {
8670
8702
  switch (stmt.type) {
8671
8703
  case "if_statement":
8672
- return processIfStatement(stmt, startId, blocks, edges, isJavaScript);
8704
+ return processIfStatement(stmt, startId, blocks, edges, dialect);
8673
8705
  case "for_statement":
8674
8706
  case "enhanced_for_statement":
8675
8707
  case "for_in_statement":
8676
8708
  case "for_of_statement":
8677
- return processForStatement(stmt, startId, blocks, edges, isJavaScript);
8709
+ case "foreach_statement":
8710
+ return processForStatement(stmt, startId, blocks, edges, dialect);
8678
8711
  case "while_statement":
8679
- return processWhileStatement(stmt, startId, blocks, edges, isJavaScript);
8712
+ return processWhileStatement(stmt, startId, blocks, edges, dialect);
8680
8713
  case "do_statement":
8681
- return processDoWhileStatement(stmt, startId, blocks, edges, isJavaScript);
8714
+ return processDoWhileStatement(stmt, startId, blocks, edges, dialect);
8682
8715
  case "try_statement":
8683
- return processTryStatement(stmt, startId, blocks, edges, isJavaScript);
8716
+ return processTryStatement(stmt, startId, blocks, edges, dialect);
8684
8717
  case "switch_expression":
8685
8718
  case "switch_statement":
8686
- return processSwitchStatement(stmt, startId, blocks, edges, isJavaScript);
8719
+ return processSwitchStatement(stmt, startId, blocks, edges, dialect);
8720
+ // C# scoped blocks add no branching — flow through the inner block so its
8721
+ // nested statements are still captured.
8722
+ case "using_statement":
8723
+ case "lock_statement":
8724
+ case "checked_statement":
8725
+ case "unsafe_statement": {
8726
+ const inner = stmt.childForFieldName("body") ?? lastBlockChild(stmt);
8727
+ if (inner) return processStatement(inner, startId, blocks, edges, dialect);
8728
+ return processSimpleStatement(stmt, startId, blocks);
8729
+ }
8687
8730
  case "block":
8688
8731
  case "statement_block":
8689
- return processStatements(stmt, startId, blocks, edges, isJavaScript);
8732
+ return processStatements(stmt, startId, blocks, edges, dialect);
8690
8733
  default:
8691
8734
  return processSimpleStatement(stmt, startId, blocks);
8692
8735
  }
8693
8736
  }
8737
+ function lastBlockChild(node) {
8738
+ for (let i2 = node.childCount - 1; i2 >= 0; i2--) {
8739
+ const c = node.child(i2);
8740
+ if (c && c.type === "block") return c;
8741
+ }
8742
+ return null;
8743
+ }
8694
8744
  function processSimpleStatement(stmt, startId, blocks) {
8695
8745
  const block = {
8696
8746
  id: startId,
@@ -8705,7 +8755,7 @@ function processSimpleStatement(stmt, startId, blocks) {
8705
8755
  nextId: startId + 1
8706
8756
  };
8707
8757
  }
8708
- function processIfStatement(stmt, startId, blocks, edges, isJavaScript) {
8758
+ function processIfStatement(stmt, startId, blocks, edges, dialect) {
8709
8759
  let currentId = startId;
8710
8760
  const condBlock = {
8711
8761
  id: currentId++,
@@ -8717,7 +8767,7 @@ function processIfStatement(stmt, startId, blocks, edges, isJavaScript) {
8717
8767
  const exitIds = [];
8718
8768
  const consequence = stmt.childForFieldName("consequence");
8719
8769
  if (consequence) {
8720
- const thenResult = processStatement(consequence, currentId, blocks, edges, isJavaScript);
8770
+ const thenResult = processStatement(consequence, currentId, blocks, edges, dialect);
8721
8771
  currentId = thenResult.nextId;
8722
8772
  edges.push({
8723
8773
  from: condBlock.id,
@@ -8728,7 +8778,7 @@ function processIfStatement(stmt, startId, blocks, edges, isJavaScript) {
8728
8778
  }
8729
8779
  const alternative = stmt.childForFieldName("alternative");
8730
8780
  if (alternative) {
8731
- const elseResult = processStatement(alternative, currentId, blocks, edges, isJavaScript);
8781
+ const elseResult = processStatement(alternative, currentId, blocks, edges, dialect);
8732
8782
  currentId = elseResult.nextId;
8733
8783
  edges.push({
8734
8784
  from: condBlock.id,
@@ -8745,7 +8795,7 @@ function processIfStatement(stmt, startId, blocks, edges, isJavaScript) {
8745
8795
  nextId: currentId
8746
8796
  };
8747
8797
  }
8748
- function processForStatement(stmt, startId, blocks, edges, isJavaScript) {
8798
+ function processForStatement(stmt, startId, blocks, edges, dialect) {
8749
8799
  let currentId = startId;
8750
8800
  const loopBlock = {
8751
8801
  id: currentId++,
@@ -8756,7 +8806,7 @@ function processForStatement(stmt, startId, blocks, edges, isJavaScript) {
8756
8806
  blocks.push(loopBlock);
8757
8807
  const body2 = stmt.childForFieldName("body");
8758
8808
  if (body2) {
8759
- const bodyResult = processStatement(body2, currentId, blocks, edges, isJavaScript);
8809
+ const bodyResult = processStatement(body2, currentId, blocks, edges, dialect);
8760
8810
  currentId = bodyResult.nextId;
8761
8811
  edges.push({
8762
8812
  from: loopBlock.id,
@@ -8778,16 +8828,16 @@ function processForStatement(stmt, startId, blocks, edges, isJavaScript) {
8778
8828
  nextId: currentId
8779
8829
  };
8780
8830
  }
8781
- function processWhileStatement(stmt, startId, blocks, edges, isJavaScript) {
8782
- return processForStatement(stmt, startId, blocks, edges, isJavaScript);
8831
+ function processWhileStatement(stmt, startId, blocks, edges, dialect) {
8832
+ return processForStatement(stmt, startId, blocks, edges, dialect);
8783
8833
  }
8784
- function processDoWhileStatement(stmt, startId, blocks, edges, isJavaScript) {
8834
+ function processDoWhileStatement(stmt, startId, blocks, edges, dialect) {
8785
8835
  let currentId = startId;
8786
8836
  const body2 = stmt.childForFieldName("body");
8787
8837
  let bodyEntryId = currentId;
8788
8838
  let bodyExitIds = [];
8789
8839
  if (body2) {
8790
- const bodyResult = processStatement(body2, currentId, blocks, edges, isJavaScript);
8840
+ const bodyResult = processStatement(body2, currentId, blocks, edges, dialect);
8791
8841
  currentId = bodyResult.nextId;
8792
8842
  bodyEntryId = bodyResult.entryId;
8793
8843
  bodyExitIds = bodyResult.exitIds;
@@ -8817,13 +8867,13 @@ function processDoWhileStatement(stmt, startId, blocks, edges, isJavaScript) {
8817
8867
  nextId: currentId
8818
8868
  };
8819
8869
  }
8820
- function processTryStatement(stmt, startId, blocks, edges, isJavaScript) {
8870
+ function processTryStatement(stmt, startId, blocks, edges, dialect) {
8821
8871
  let currentId = startId;
8822
8872
  const exitIds = [];
8823
8873
  const body2 = stmt.childForFieldName("body");
8824
8874
  let tryEntryId = -1;
8825
8875
  if (body2) {
8826
- const bodyResult = processStatements(body2, currentId, blocks, edges, isJavaScript);
8876
+ const bodyResult = processStatements(body2, currentId, blocks, edges, dialect);
8827
8877
  currentId = bodyResult.nextId;
8828
8878
  tryEntryId = bodyResult.entryId;
8829
8879
  exitIds.push(...bodyResult.exitIds);
@@ -8833,7 +8883,7 @@ function processTryStatement(stmt, startId, blocks, edges, isJavaScript) {
8833
8883
  if (child?.type === "catch_clause") {
8834
8884
  const catchBody = child.childForFieldName("body");
8835
8885
  if (catchBody) {
8836
- const catchResult = processStatements(catchBody, currentId, blocks, edges, isJavaScript);
8886
+ const catchResult = processStatements(catchBody, currentId, blocks, edges, dialect);
8837
8887
  currentId = catchResult.nextId;
8838
8888
  if (tryEntryId !== -1) {
8839
8889
  edges.push({
@@ -8846,9 +8896,18 @@ function processTryStatement(stmt, startId, blocks, edges, isJavaScript) {
8846
8896
  }
8847
8897
  }
8848
8898
  }
8849
- const finallyClause = stmt.childForFieldName("finally") ?? stmt.childForFieldName("finalizer");
8899
+ let finallyClause = stmt.childForFieldName("finally") ?? stmt.childForFieldName("finalizer");
8900
+ if (!finallyClause && dialect === "csharp") {
8901
+ for (let i2 = 0; i2 < stmt.childCount; i2++) {
8902
+ const child = stmt.child(i2);
8903
+ if (child?.type === "finally_clause") {
8904
+ finallyClause = lastBlockChild(child);
8905
+ break;
8906
+ }
8907
+ }
8908
+ }
8850
8909
  if (finallyClause) {
8851
- const finallyResult = processStatements(finallyClause, currentId, blocks, edges, isJavaScript);
8910
+ const finallyResult = processStatements(finallyClause, currentId, blocks, edges, dialect);
8852
8911
  currentId = finallyResult.nextId;
8853
8912
  for (const exitId of exitIds) {
8854
8913
  edges.push({
@@ -8869,7 +8928,7 @@ function processTryStatement(stmt, startId, blocks, edges, isJavaScript) {
8869
8928
  nextId: currentId
8870
8929
  };
8871
8930
  }
8872
- function processSwitchStatement(stmt, startId, blocks, edges, isJavaScript) {
8931
+ function processSwitchStatement(stmt, startId, blocks, edges, dialect) {
8873
8932
  let currentId = startId;
8874
8933
  const switchBlock = {
8875
8934
  id: currentId++,
@@ -8881,11 +8940,11 @@ function processSwitchStatement(stmt, startId, blocks, edges, isJavaScript) {
8881
8940
  const exitIds = [];
8882
8941
  const body2 = stmt.childForFieldName("body");
8883
8942
  if (body2) {
8884
- const caseTypes = isJavaScript ? ["switch_case", "switch_default"] : ["switch_block_statement_group", "switch_rule"];
8943
+ const caseTypes = dialect === "js" ? ["switch_case", "switch_default"] : dialect === "csharp" ? ["switch_section"] : ["switch_block_statement_group", "switch_rule"];
8885
8944
  for (let i2 = 0; i2 < body2.childCount; i2++) {
8886
8945
  const child = body2.child(i2);
8887
8946
  if (child && caseTypes.includes(child.type)) {
8888
- const caseResult = processStatements(child, currentId, blocks, edges, isJavaScript);
8947
+ const caseResult = processStatements(child, currentId, blocks, edges, dialect);
8889
8948
  currentId = caseResult.nextId;
8890
8949
  if (caseResult.entryId !== -1) {
8891
8950
  edges.push({
@@ -8915,7 +8974,7 @@ function buildBashCFG(tree, startId, cache) {
8915
8974
  for (const func2 of functions) {
8916
8975
  const body2 = func2.childForFieldName("body");
8917
8976
  if (!body2) continue;
8918
- const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, false);
8977
+ const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, "java");
8919
8978
  allBlocks.push(...blocks);
8920
8979
  allEdges.push(...edges);
8921
8980
  blockIdCounter = nextId;
@@ -8938,7 +8997,7 @@ function buildBashCFG(tree, startId, cache) {
8938
8997
  let lastExitIds = [];
8939
8998
  let firstBlockId = -1;
8940
8999
  for (const stmt of topLevelStatements) {
8941
- const result = processStatement(stmt, blockIdCounter, allBlocks, allEdges, false);
9000
+ const result = processStatement(stmt, blockIdCounter, allBlocks, allEdges, "java");
8942
9001
  blockIdCounter = result.nextId;
8943
9002
  if (firstBlockId === -1) {
8944
9003
  firstBlockId = result.entryId;
@@ -8982,7 +9041,8 @@ function isBashStatement(node) {
8982
9041
  ]);
8983
9042
  return bashStatementTypes.has(node.type);
8984
9043
  }
8985
- function isStatement(node, isJavaScript) {
9044
+ function isStatement(node, dialect) {
9045
+ if (dialect === "csharp") return csharpStatementTypes.has(node.type);
8986
9046
  const javaStatementTypes = /* @__PURE__ */ new Set([
8987
9047
  "local_variable_declaration",
8988
9048
  "expression_statement",
@@ -9026,8 +9086,30 @@ function isStatement(node, isJavaScript) {
9026
9086
  "export_statement",
9027
9087
  "import_statement"
9028
9088
  ]);
9029
- return isJavaScript ? jsStatementTypes.has(node.type) : javaStatementTypes.has(node.type);
9030
- }
9089
+ return dialect === "js" ? jsStatementTypes.has(node.type) : javaStatementTypes.has(node.type);
9090
+ }
9091
+ var csharpStatementTypes = /* @__PURE__ */ new Set([
9092
+ "local_declaration_statement",
9093
+ "expression_statement",
9094
+ "if_statement",
9095
+ "for_statement",
9096
+ "foreach_statement",
9097
+ "while_statement",
9098
+ "do_statement",
9099
+ "try_statement",
9100
+ "switch_statement",
9101
+ "return_statement",
9102
+ "throw_statement",
9103
+ "break_statement",
9104
+ "continue_statement",
9105
+ "using_statement",
9106
+ "lock_statement",
9107
+ "checked_statement",
9108
+ "unsafe_statement",
9109
+ "yield_statement",
9110
+ "goto_statement",
9111
+ "block"
9112
+ ]);
9031
9113
  function buildGoCFG(tree, blockIdCounter, cache) {
9032
9114
  const allBlocks = [];
9033
9115
  const allEdges = [];
@@ -9038,7 +9120,7 @@ function buildGoCFG(tree, blockIdCounter, cache) {
9038
9120
  for (const func2 of functions) {
9039
9121
  const body2 = func2.childForFieldName("body");
9040
9122
  if (!body2 || body2.type !== "block") continue;
9041
- const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, false);
9123
+ const { blocks, edges, nextId } = buildMethodCFG(body2, blockIdCounter, "java");
9042
9124
  allBlocks.push(...blocks);
9043
9125
  allEdges.push(...edges);
9044
9126
  blockIdCounter = nextId;
@@ -13118,6 +13200,11 @@ var DEFAULT_SINKS = [
13118
13200
  // ADO.NET `cmd.CommandText = "…" + x` — emitted as a synthetic call by
13119
13201
  // extractCSharpCalls (property-assignment sink; the ctor-arg sink misses it).
13120
13202
  { method: "CommandText", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13203
+ // `DirectorySearcher.Filter = "(uid=" + x + ")"` — LDAP injection (cognium-ai#272).
13204
+ // Emitted as a synthetic call by extractCSharpCalls ONLY when the receiver
13205
+ // resolves to a DirectorySearcher, so this classless entry never over-matches
13206
+ // other `.Filter =` assignments (DataView/BindingSource/collection filters).
13207
+ { method: "Filter", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13121
13208
  // ADO.NET `cmd.Execute*()` — object-carried sink (cognium-dev#271). The taint
13122
13209
  // rides the SqlCommand receiver (CommandText set from tainted data); the
13123
13210
  // receiver is surfaced as arg[0] by extractCSharpCalls, and the command object
@@ -13134,6 +13221,9 @@ var DEFAULT_SINKS = [
13134
13221
  // injectable — the second is the argv path where taint rides arg[1] (#276).
13135
13222
  { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13136
13223
  { method: "ProcessStartInfo", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
13224
+ // P/Invoke of libc `system(cmd)` — lowercase `system` in C# is virtually
13225
+ // always the imported shell entry point (cognium-ai#275 interop/IlPinvoke).
13226
+ { method: "system", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13137
13227
  // C# path traversal — System.IO file APIs (CWE-22). Distinctive method names.
13138
13228
  { method: "ReadAllText", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13139
13229
  { method: "ReadAllBytes", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -13162,6 +13252,9 @@ var DEFAULT_SINKS = [
13162
13252
  { method: "Delete", class: "Directory", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13163
13253
  { method: "GetFiles", class: "Directory", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13164
13254
  { method: "EnumerateFiles", class: "Directory", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13255
+ // ASP.NET `ControllerBase.PhysicalFile(path, contentType)` serves a file from
13256
+ // an absolute disk path — attacker-controllable path is CWE-22 (cognium-ai#326/#275).
13257
+ { method: "PhysicalFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13165
13258
  // C# SSRF — HttpClient / WebClient / WebRequest (CWE-918).
13166
13259
  { method: "GetAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13167
13260
  { method: "PostAsync", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -13200,6 +13293,14 @@ var DEFAULT_SINKS = [
13200
13293
  { method: "Raw", class: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13201
13294
  { method: "Write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13202
13295
  { method: "HtmlString", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13296
+ // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13297
+ // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13298
+ { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13299
+ // `String.Format(fmt, …)` with an attacker-controlled FORMAT string — CWE-134.
13300
+ // Taint-gated on arg 0 (the format), so ordinary `String.Format("...", user)` where
13301
+ // only an argument is tainted never fires. .NET composite formatting can't corrupt
13302
+ // memory, so medium (FormatException DoS / unintended arg access), unlike C printf.
13303
+ { method: "Format", class: "String", type: "format_string", cwe: "CWE-134", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13203
13304
  // C# LDAP injection — System.DirectoryServices (CWE-90). The user-built
13204
13305
  // filter is the constructor argument.
13205
13306
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -13238,6 +13339,10 @@ var DEFAULT_SINKS = [
13238
13339
  { method: "SelectSingleNode", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
13239
13340
  { method: "SelectNodes", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
13240
13341
  { method: "Compile", class: "XPathExpression", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
13342
+ // XPathNavigator.Select/Evaluate — class-scoped (both names collide with LINQ
13343
+ // `.Select`/`.Evaluate`, so they must resolve to an XPathNavigator receiver).
13344
+ { method: "Select", class: "XPathNavigator", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
13345
+ { method: "Evaluate", class: "XPathNavigator", type: "xpath_injection", cwe: "CWE-643", severity: "high", arg_positions: [0], languages: ["csharp"] },
13241
13346
  // C# XXE — untrusted XML into a parser without DTD hardening (CWE-611).
13242
13347
  { method: "LoadXml", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0], languages: ["csharp"] },
13243
13348
  { method: "Load", class: "XmlDocument", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0], languages: ["csharp"] },
@@ -14564,33 +14669,56 @@ function isSafeJSChildProcessCall(call, pattern, language) {
14564
14669
  if (SHELL_PROGRAMS.has(program)) return false;
14565
14670
  return true;
14566
14671
  }
14567
- function isSafeCSharpProcessStartCall(call, pattern, language) {
14672
+ var CSHARP_SHELL_PROGRAMS = /* @__PURE__ */ new Set([
14673
+ "sh",
14674
+ "bash",
14675
+ "zsh",
14676
+ "dash",
14677
+ "ash",
14678
+ "ksh",
14679
+ "cmd",
14680
+ "powershell",
14681
+ "pwsh"
14682
+ ]);
14683
+ function isConstNonShellExe(raw) {
14684
+ if (!raw) return false;
14685
+ const t = raw.trim();
14686
+ if (!/^@?"[^"]*"$/.test(t)) return false;
14687
+ const program = (t.replace(/^@?"|"$/g, "").split(/[\\/]/).pop() ?? "").toLowerCase().replace(/\.exe$/, "");
14688
+ return !CSHARP_SHELL_PROGRAMS.has(program);
14689
+ }
14690
+ var PSI_CTOR_EXE_RE = /\bnew\s+ProcessStartInfo\s*(?:<[^>]*>)?\s*\(\s*(@?"[^"]*")/;
14691
+ function processStartInfoExe(expr, sourceLines) {
14692
+ const inline = PSI_CTOR_EXE_RE.exec(expr);
14693
+ if (inline) return inline[1];
14694
+ if (sourceLines && /^[A-Za-z_]\w*$/.test(expr.trim())) {
14695
+ const varName = expr.trim();
14696
+ const assignRe = new RegExp(
14697
+ `\\b${varName}\\s*=\\s*new\\s+ProcessStartInfo\\s*(?:<[^>]*>)?\\s*\\(\\s*(@?"[^"]*")`
14698
+ );
14699
+ for (const line of sourceLines) {
14700
+ const m = assignRe.exec(line);
14701
+ if (m) return m[1];
14702
+ }
14703
+ }
14704
+ return null;
14705
+ }
14706
+ function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
14568
14707
  if (language !== "csharp") return false;
14569
14708
  if (pattern.type !== "command_injection") return false;
14570
- if (call.method_name !== "Start") return false;
14571
- if (call.arguments.length < 2) return false;
14572
- const fileArg = call.arguments.find((a) => a.position === 0);
14573
- if (!fileArg) return false;
14574
- let raw;
14575
- if (fileArg.literal !== null && fileArg.literal !== void 0) {
14576
- raw = String(fileArg.literal).trim();
14577
- } else {
14578
- raw = (fileArg.expression ?? "").trim();
14709
+ const method = call.method_name;
14710
+ if (method !== "Start" && method !== "ProcessStartInfo") return false;
14711
+ if (call.arguments.length >= 2) {
14712
+ const fileArg = call.arguments.find((a) => a.position === 0);
14713
+ const raw = fileArg?.literal != null ? String(fileArg.literal) : fileArg?.expression;
14714
+ return isConstNonShellExe(raw);
14579
14715
  }
14580
- if (!/^@?"[^"]*"$/.test(raw)) return false;
14581
- const program = (raw.replace(/^@?"|"$/g, "").split(/[\\/]/).pop() ?? "").toLowerCase().replace(/\.exe$/, "");
14582
- const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
14583
- "sh",
14584
- "bash",
14585
- "zsh",
14586
- "dash",
14587
- "ash",
14588
- "ksh",
14589
- "cmd",
14590
- "powershell",
14591
- "pwsh"
14592
- ]);
14593
- return !SHELL_PROGRAMS.has(program);
14716
+ if (method === "Start" && call.arguments.length === 1) {
14717
+ const arg0 = call.arguments.find((a) => a.position === 0);
14718
+ const expr = (arg0?.expression ?? "").trim();
14719
+ return isConstNonShellExe(processStartInfoExe(expr, sourceLines));
14720
+ }
14721
+ return false;
14594
14722
  }
14595
14723
  function isSafeRustCommandCall(call, pattern, language) {
14596
14724
  if (language !== "rust") return false;
@@ -14847,7 +14975,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
14847
14975
  if (isSafeJSChildProcessCall(call, pattern, language)) {
14848
14976
  continue;
14849
14977
  }
14850
- if (isSafeCSharpProcessStartCall(call, pattern, language)) {
14978
+ if (isSafeCSharpProcessStartCall(call, pattern, language, sourceLines)) {
14851
14979
  continue;
14852
14980
  }
14853
14981
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at, types)) {
@@ -224,15 +224,29 @@ function extractCSharpCalls(tree, cache) {
224
224
  if (left?.type !== 'member_access_expression')
225
225
  continue;
226
226
  const nameNode = left.childForFieldName('name');
227
- if (!nameNode || getNodeText(nameNode) !== 'CommandText')
227
+ if (!nameNode)
228
228
  continue;
229
+ const propName = getNodeText(nameNode);
230
+ const exprNode = left.childForFieldName('expression');
231
+ // `CommandText` is distinctive to ADO.NET commands, so it fires classless.
232
+ // `Filter` is a common property name, so it is class-gated to a resolved
233
+ // `DirectorySearcher` receiver (cognium-ai#272 LDAP injection) — this keeps
234
+ // the classless registry sink from over-matching unrelated `.Filter =`.
235
+ if (propName === 'Filter') {
236
+ const recv = exprNode ? getNodeText(exprNode) : null;
237
+ const recvType = recv ? typeMap.get(recv) : undefined;
238
+ if (recvType !== 'DirectorySearcher')
239
+ continue;
240
+ }
241
+ else if (propName !== 'CommandText') {
242
+ continue;
243
+ }
229
244
  const right = asn.childForFieldName('right');
230
245
  if (!right)
231
246
  continue;
232
247
  const rhsText = getNodeText(right);
233
- const exprNode = left.childForFieldName('expression');
234
248
  calls.push({
235
- method_name: 'CommandText',
249
+ method_name: propName,
236
250
  receiver: exprNode ? getNodeText(exprNode) : null,
237
251
  receiver_type: null,
238
252
  receiver_type_fqn: null,