circle-ir 4.4.0 → 4.6.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.
@@ -5941,6 +5941,14 @@ function findFirstDescendant(node, type) {
5941
5941
  }
5942
5942
  return null;
5943
5943
  }
5944
+ var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
5945
+ "ExecuteScalar",
5946
+ "ExecuteReader",
5947
+ "ExecuteNonQuery",
5948
+ "ExecuteScalarAsync",
5949
+ "ExecuteReaderAsync",
5950
+ "ExecuteNonQueryAsync"
5951
+ ]);
5944
5952
  function extractCSharpCalls(tree, cache) {
5945
5953
  const calls = [];
5946
5954
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -5958,12 +5966,16 @@ function extractCSharpCalls(tree, cache) {
5958
5966
  methodName = getNodeText(fn);
5959
5967
  }
5960
5968
  const argsNode = inv.childForFieldName("arguments");
5969
+ let args2 = argsNode ? extractCSharpArguments(argsNode) : [];
5970
+ if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
5971
+ args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args2];
5972
+ }
5961
5973
  calls.push({
5962
5974
  method_name: methodName,
5963
5975
  receiver,
5964
5976
  receiver_type: receiver ? typeMap.get(receiver) ?? null : null,
5965
5977
  receiver_type_fqn: null,
5966
- arguments: argsNode ? extractCSharpArguments(argsNode) : [],
5978
+ arguments: args2,
5967
5979
  location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
5968
5980
  in_method: findEnclosingMethod(inv)
5969
5981
  });
@@ -13770,6 +13782,16 @@ var DEFAULT_SINKS = [
13770
13782
  // ADO.NET `cmd.CommandText = "…" + x` — emitted as a synthetic call by
13771
13783
  // extractCSharpCalls (property-assignment sink; the ctor-arg sink misses it).
13772
13784
  { method: "CommandText", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13785
+ // ADO.NET `cmd.Execute*()` — object-carried sink (cognium-dev#271). The taint
13786
+ // rides the SqlCommand receiver (CommandText set from tainted data); the
13787
+ // receiver is surfaced as arg[0] by extractCSharpCalls, and the command object
13788
+ // is tainted in taint-propagation when its CommandText takes a tainted value.
13789
+ { method: "ExecuteScalar", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13790
+ { method: "ExecuteReader", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13791
+ { method: "ExecuteNonQuery", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13792
+ { method: "ExecuteScalarAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13793
+ { method: "ExecuteReaderAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13794
+ { method: "ExecuteNonQueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13773
13795
  // C# command injection — Process.Start / ProcessStartInfo (CWE-78).
13774
13796
  { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13775
13797
  { method: "ProcessStartInfo", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
@@ -27013,15 +27035,17 @@ function findCSharpRequestSources(sourceCode, language) {
27013
27035
  const lines = sourceCode.split("\n");
27014
27036
  const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>\[\]]*\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);?\s*$/;
27015
27037
  const requestReadRe = /\bRequest\s*\.\s*(?:Query|Form|Headers|Cookies|QueryString|RouteValues|Body|Files|Params)\b/;
27038
+ const consoleReadRe = /\bConsole\s*\.\s*ReadLine\s*\(/;
27016
27039
  for (let i2 = 0; i2 < lines.length; i2++) {
27017
27040
  const m = assignRe.exec(lines[i2]);
27018
27041
  if (!m) continue;
27019
27042
  const [, varName, rhs] = m;
27020
- if (!requestReadRe.test(rhs)) continue;
27043
+ const type = requestReadRe.test(rhs) ? "http_param" : consoleReadRe.test(rhs) ? "io_input" : null;
27044
+ if (!type) continue;
27021
27045
  const lineNumber = i2 + 1;
27022
27046
  if (sources.some((s) => s.line === lineNumber && s.variable === varName)) continue;
27023
27047
  sources.push({
27024
- type: "http_param",
27048
+ type,
27025
27049
  location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
27026
27050
  severity: "high",
27027
27051
  line: lineNumber,
@@ -36385,6 +36409,31 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
36385
36409
  }
36386
36410
  }
36387
36411
  }
36412
+ if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0) {
36413
+ const lines = code.split("\n");
36414
+ const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
36415
+ const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
36416
+ let added = true;
36417
+ let guard = 0;
36418
+ while (added && guard < lines.length + 2) {
36419
+ added = false;
36420
+ guard++;
36421
+ const known = new Set(sourcesWithVar.map((s) => s.variable));
36422
+ for (let i2 = 0; i2 < lines.length; i2++) {
36423
+ const m = ctRe.exec(lines[i2]);
36424
+ if (!m) continue;
36425
+ const [, obj, rhs] = m;
36426
+ if (known.has(obj)) continue;
36427
+ const codePart = rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ");
36428
+ const rhsTainted = sourcesWithVar.some(
36429
+ (s) => new RegExp(`(?<![\\p{L}\\p{N}_])${s.variable.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}(?![\\p{L}\\p{N}_])`, "u").test(codePart)
36430
+ );
36431
+ if (!rhsTainted) continue;
36432
+ sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
36433
+ added = true;
36434
+ }
36435
+ }
36436
+ }
36388
36437
  const reCache = /* @__PURE__ */ new Map();
36389
36438
  for (const s of sourcesWithVar) {
36390
36439
  if (reCache.has(s.variable)) continue;
@@ -43113,6 +43162,45 @@ function methodAcceptsHtmlShapedParam(method) {
43113
43162
  return false;
43114
43163
  }
43115
43164
 
43165
+ // src/analysis/passes/insecure-deserialization-config-pass.ts
43166
+ var ANY_TYPE_PERMISSION_RE = /\bAnyTypePermission\b/;
43167
+ var InsecureDeserializationConfigPass = class {
43168
+ name = "insecure-deserialization-config";
43169
+ category = "security";
43170
+ run(ctx) {
43171
+ const { graph, language } = ctx;
43172
+ if (language !== "java") return { findings: [] };
43173
+ const file = graph.ir.meta.file;
43174
+ const findings = [];
43175
+ for (const call of graph.ir.calls) {
43176
+ if (!this.isPermissiveXStreamConfig(call)) continue;
43177
+ const line = call.location.line;
43178
+ const api = `addPermission(${call.arguments[0]?.expression ?? "AnyTypePermission"})`;
43179
+ findings.push({ line, api });
43180
+ ctx.addFinding({
43181
+ id: `${this.name}-${file}-${line}`,
43182
+ pass: this.name,
43183
+ category: this.category,
43184
+ rule_id: this.name,
43185
+ cwe: "CWE-502",
43186
+ severity: "high",
43187
+ level: "error",
43188
+ message: "XStream configured with AnyTypePermission (grant-all): untrusted XML passed to fromXML/unmarshal can instantiate arbitrary types (deserialization RCE)",
43189
+ file,
43190
+ line,
43191
+ fix: "Replace AnyTypePermission with an explicit type allow-list: xstream.addPermission(NoTypePermission.NONE) then allowTypes/allowTypesByWildcard for the classes you deserialize.",
43192
+ evidence: { api, language }
43193
+ });
43194
+ }
43195
+ return { findings };
43196
+ }
43197
+ isPermissiveXStreamConfig(call) {
43198
+ if (call.method_name !== "addPermission") return false;
43199
+ const arg0 = call.arguments[0]?.expression;
43200
+ return typeof arg0 === "string" && ANY_TYPE_PERMISSION_RE.test(arg0);
43201
+ }
43202
+ };
43203
+
43116
43204
  // src/analysis/passes/plaintext-password-storage-pass.ts
43117
43205
  function isWriteStorageCall(call, language) {
43118
43206
  const method = call.method_name ?? "";
@@ -45469,6 +45557,7 @@ async function analyze(code, filePath, language, options = {}) {
45469
45557
  if (!disabledPasses.has("info-disclosure-stacktrace")) pipeline.add(new InfoDisclosureStacktracePass());
45470
45558
  if (!disabledPasses.has("unrestricted-file-upload")) pipeline.add(new UnrestrictedFileUploadPass());
45471
45559
  if (!disabledPasses.has("missing-sanitizer-gate")) pipeline.add(new MissingSanitizerGatePass());
45560
+ if (!disabledPasses.has("insecure-deserialization-config")) pipeline.add(new InsecureDeserializationConfigPass());
45472
45561
  const { results, findings } = pipeline.run(graph, code, language, config);
45473
45562
  const sinkFilter = results.get("sink-filter");
45474
45563
  const interProc = results.get("interprocedural");
@@ -5987,6 +5987,14 @@ function findFirstDescendant(node, type) {
5987
5987
  }
5988
5988
  return null;
5989
5989
  }
5990
+ var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
5991
+ "ExecuteScalar",
5992
+ "ExecuteReader",
5993
+ "ExecuteNonQuery",
5994
+ "ExecuteScalarAsync",
5995
+ "ExecuteReaderAsync",
5996
+ "ExecuteNonQueryAsync"
5997
+ ]);
5990
5998
  function extractCSharpCalls(tree, cache) {
5991
5999
  const calls = [];
5992
6000
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -6004,12 +6012,16 @@ function extractCSharpCalls(tree, cache) {
6004
6012
  methodName = getNodeText(fn);
6005
6013
  }
6006
6014
  const argsNode = inv.childForFieldName("arguments");
6015
+ let args2 = argsNode ? extractCSharpArguments(argsNode) : [];
6016
+ if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
6017
+ args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args2];
6018
+ }
6007
6019
  calls.push({
6008
6020
  method_name: methodName,
6009
6021
  receiver,
6010
6022
  receiver_type: receiver ? typeMap.get(receiver) ?? null : null,
6011
6023
  receiver_type_fqn: null,
6012
- arguments: argsNode ? extractCSharpArguments(argsNode) : [],
6024
+ arguments: args2,
6013
6025
  location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
6014
6026
  in_method: findEnclosingMethod(inv)
6015
6027
  });
@@ -13165,6 +13177,16 @@ var DEFAULT_SINKS = [
13165
13177
  // ADO.NET `cmd.CommandText = "…" + x` — emitted as a synthetic call by
13166
13178
  // extractCSharpCalls (property-assignment sink; the ctor-arg sink misses it).
13167
13179
  { method: "CommandText", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13180
+ // ADO.NET `cmd.Execute*()` — object-carried sink (cognium-dev#271). The taint
13181
+ // rides the SqlCommand receiver (CommandText set from tainted data); the
13182
+ // receiver is surfaced as arg[0] by extractCSharpCalls, and the command object
13183
+ // is tainted in taint-propagation when its CommandText takes a tainted value.
13184
+ { method: "ExecuteScalar", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13185
+ { method: "ExecuteReader", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13186
+ { method: "ExecuteNonQuery", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13187
+ { method: "ExecuteScalarAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13188
+ { method: "ExecuteReaderAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13189
+ { method: "ExecuteNonQueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13168
13190
  // C# command injection — Process.Start / ProcessStartInfo (CWE-78).
13169
13191
  { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13170
13192
  { method: "ProcessStartInfo", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
@@ -5921,6 +5921,14 @@ function findFirstDescendant(node, type) {
5921
5921
  }
5922
5922
  return null;
5923
5923
  }
5924
+ var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
5925
+ "ExecuteScalar",
5926
+ "ExecuteReader",
5927
+ "ExecuteNonQuery",
5928
+ "ExecuteScalarAsync",
5929
+ "ExecuteReaderAsync",
5930
+ "ExecuteNonQueryAsync"
5931
+ ]);
5924
5932
  function extractCSharpCalls(tree, cache) {
5925
5933
  const calls = [];
5926
5934
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -5938,12 +5946,16 @@ function extractCSharpCalls(tree, cache) {
5938
5946
  methodName = getNodeText(fn);
5939
5947
  }
5940
5948
  const argsNode = inv.childForFieldName("arguments");
5949
+ let args2 = argsNode ? extractCSharpArguments(argsNode) : [];
5950
+ if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
5951
+ args2 = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args2];
5952
+ }
5941
5953
  calls.push({
5942
5954
  method_name: methodName,
5943
5955
  receiver,
5944
5956
  receiver_type: receiver ? typeMap.get(receiver) ?? null : null,
5945
5957
  receiver_type_fqn: null,
5946
- arguments: argsNode ? extractCSharpArguments(argsNode) : [],
5958
+ arguments: args2,
5947
5959
  location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
5948
5960
  in_method: findEnclosingMethod(inv)
5949
5961
  });
@@ -13099,6 +13111,16 @@ var DEFAULT_SINKS = [
13099
13111
  // ADO.NET `cmd.CommandText = "…" + x` — emitted as a synthetic call by
13100
13112
  // extractCSharpCalls (property-assignment sink; the ctor-arg sink misses it).
13101
13113
  { method: "CommandText", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13114
+ // ADO.NET `cmd.Execute*()` — object-carried sink (cognium-dev#271). The taint
13115
+ // rides the SqlCommand receiver (CommandText set from tainted data); the
13116
+ // receiver is surfaced as arg[0] by extractCSharpCalls, and the command object
13117
+ // is tainted in taint-propagation when its CommandText takes a tainted value.
13118
+ { method: "ExecuteScalar", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13119
+ { method: "ExecuteReader", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13120
+ { method: "ExecuteNonQuery", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13121
+ { method: "ExecuteScalarAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13122
+ { method: "ExecuteReaderAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13123
+ { method: "ExecuteNonQueryAsync", type: "sql_injection", cwe: "CWE-89", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13102
13124
  // C# command injection — Process.Start / ProcessStartInfo (CWE-78).
13103
13125
  { method: "Start", class: "Process", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13104
13126
  { method: "ProcessStartInfo", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
@@ -155,6 +155,11 @@ function findFirstDescendant(node, type) {
155
155
  }
156
156
  return null;
157
157
  }
158
+ /** ADO.NET command-execution methods whose taint rides the receiver object. */
159
+ const CSHARP_COMMAND_EXECUTE_METHODS = new Set([
160
+ 'ExecuteScalar', 'ExecuteReader', 'ExecuteNonQuery',
161
+ 'ExecuteScalarAsync', 'ExecuteReaderAsync', 'ExecuteNonQueryAsync',
162
+ ]);
158
163
  function extractCSharpCalls(tree, cache) {
159
164
  const calls = [];
160
165
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -173,12 +178,21 @@ function extractCSharpCalls(tree, cache) {
173
178
  methodName = getNodeText(fn);
174
179
  }
175
180
  const argsNode = inv.childForFieldName('arguments');
181
+ let args = argsNode ? extractCSharpArguments(argsNode) : [];
182
+ // ADO.NET `Execute*()` carries taint via the RECEIVER (a SqlCommand whose
183
+ // CommandText was set from tainted data), not an argument. Surface the
184
+ // receiver as arg[0] so the arg-based flow scan can match a tainted command
185
+ // object (cognium-dev#271 — the canonical `cmd.CommandText=…; cmd.Execute*()`
186
+ // Juliet shape). The command-object taint is seeded in taint-propagation.
187
+ if (receiver && CSHARP_COMMAND_EXECUTE_METHODS.has(methodName)) {
188
+ args = [{ position: 0, expression: receiver, variable: receiver, literal: null, value: null }, ...args];
189
+ }
176
190
  calls.push({
177
191
  method_name: methodName,
178
192
  receiver,
179
193
  receiver_type: receiver ? (typeMap.get(receiver) ?? null) : null,
180
194
  receiver_type_fqn: null,
181
- arguments: argsNode ? extractCSharpArguments(argsNode) : [],
195
+ arguments: args,
182
196
  location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
183
197
  in_method: findEnclosingMethod(inv),
184
198
  });