circle-ir 4.9.27 → 4.9.29

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 (31) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +58 -5
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/findings.d.ts.map +1 -1
  5. package/dist/analysis/findings.js +130 -0
  6. package/dist/analysis/findings.js.map +1 -1
  7. package/dist/analysis/interprocedural.d.ts.map +1 -1
  8. package/dist/analysis/interprocedural.js +63 -0
  9. package/dist/analysis/interprocedural.js.map +1 -1
  10. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  11. package/dist/analysis/passes/language-sources-pass.js +162 -8
  12. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  13. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  14. package/dist/analysis/passes/taint-propagation-pass.js +214 -16
  15. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  16. package/dist/analysis/passes/variable-shadowing-pass.d.ts.map +1 -1
  17. package/dist/analysis/passes/variable-shadowing-pass.js +15 -0
  18. package/dist/analysis/passes/variable-shadowing-pass.js.map +1 -1
  19. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  20. package/dist/analysis/taint-matcher.js +62 -1
  21. package/dist/analysis/taint-matcher.js.map +1 -1
  22. package/dist/browser/circle-ir.js +486 -33
  23. package/dist/core/circle-ir-core.cjs +173 -13
  24. package/dist/core/circle-ir-core.js +173 -13
  25. package/dist/core/extractors/calls.js +155 -1
  26. package/dist/core/extractors/calls.js.map +1 -1
  27. package/dist/core/extractors/types.js +50 -7
  28. package/dist/core/extractors/types.js.map +1 -1
  29. package/dist/types/config.d.ts +14 -0
  30. package/dist/types/config.d.ts.map +1 -1
  31. package/package.json +1 -1
@@ -4480,6 +4480,21 @@ function extractTypes(tree, cache, language) {
4480
4480
  }
4481
4481
  return extractJavaTypes(tree, cache);
4482
4482
  }
4483
+ function csharpMemberNodes(body2) {
4484
+ if (!body2) return [];
4485
+ const out2 = [];
4486
+ const visit = (parent, depth) => {
4487
+ if (depth > 32) return;
4488
+ for (let i2 = 0; i2 < parent.childCount; i2++) {
4489
+ const c = parent.child(i2);
4490
+ if (!c) continue;
4491
+ if (c.type.startsWith("preproc_")) visit(c, depth + 1);
4492
+ else out2.push(c);
4493
+ }
4494
+ };
4495
+ visit(body2, 0);
4496
+ return out2;
4497
+ }
4483
4498
  function extractCSharpTypes(tree, cache) {
4484
4499
  const types = [];
4485
4500
  const KINDS = {
@@ -4496,9 +4511,8 @@ function extractCSharpTypes(tree, cache) {
4496
4511
  const methods = [];
4497
4512
  const fields = extractCSharpFields(body2);
4498
4513
  if (body2) {
4499
- for (let i2 = 0; i2 < body2.childCount; i2++) {
4500
- const m = body2.child(i2);
4501
- if (!m || m.type !== "method_declaration") continue;
4514
+ for (const m of csharpMemberNodes(body2)) {
4515
+ if (m.type !== "method_declaration") continue;
4502
4516
  const mName = m.childForFieldName("name");
4503
4517
  const paramList = m.childForFieldName("parameters");
4504
4518
  const parameters = [];
@@ -4549,9 +4563,7 @@ function extractCSharpTypes(tree, cache) {
4549
4563
  function extractCSharpFields(body2) {
4550
4564
  const fields = [];
4551
4565
  if (!body2) return fields;
4552
- for (let i2 = 0; i2 < body2.childCount; i2++) {
4553
- const c = body2.child(i2);
4554
- if (!c) continue;
4566
+ for (const c of csharpMemberNodes(body2)) {
4555
4567
  if (c.type === "field_declaration") {
4556
4568
  let varDecl = null;
4557
4569
  for (let k = 0; k < c.childCount; k++) {
@@ -6085,6 +6097,13 @@ function extractCalls(tree, cache, language) {
6085
6097
  function buildCSharpReceiverTypeMap(tree, cache) {
6086
6098
  const map = /* @__PURE__ */ new Map();
6087
6099
  const simple = (t) => t.replace(/<[^>]*>/g, "").split(".").pop()?.trim() ?? t;
6100
+ for (const prm of getNodesFromCache(tree.rootNode, "parameter", cache)) {
6101
+ const nameNode = prm.childForFieldName("name");
6102
+ const typeNode = prm.childForFieldName("type");
6103
+ if (nameNode?.type !== "identifier" || !typeNode) continue;
6104
+ const t = getNodeText(typeNode);
6105
+ if (t && t !== "var") map.set(getNodeText(nameNode), simple(t));
6106
+ }
6088
6107
  for (const vd of getNodesFromCache(tree.rootNode, "variable_declaration", cache)) {
6089
6108
  const typeNode = vd.childForFieldName("type");
6090
6109
  const declaredType = typeNode ? getNodeText(typeNode) : null;
@@ -6114,6 +6133,7 @@ function findFirstDescendant(node, type) {
6114
6133
  }
6115
6134
  return null;
6116
6135
  }
6136
+ var CSHARP_RESPONSE_TYPES = /* @__PURE__ */ new Set(["HttpResponse", "HttpResponseBase"]);
6117
6137
  var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
6118
6138
  "ExecuteScalar",
6119
6139
  "ExecuteReader",
@@ -6230,6 +6250,31 @@ function extractCSharpCalls(tree, cache) {
6230
6250
  });
6231
6251
  continue;
6232
6252
  }
6253
+ if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
6254
+ const creation = asn.parent.parent;
6255
+ if (creation?.type !== "object_creation_expression") continue;
6256
+ const ctorType = creation.childForFieldName("type");
6257
+ const right2 = asn.childForFieldName("right");
6258
+ if (!ctorType || !right2) continue;
6259
+ const typeName = csharpBareName(ctorType);
6260
+ const rhsText2 = getNodeText(right2);
6261
+ calls.push({
6262
+ method_name: getNodeText(left),
6263
+ receiver: typeName,
6264
+ receiver_type: typeName,
6265
+ receiver_type_fqn: null,
6266
+ arguments: [{
6267
+ position: 0,
6268
+ expression: rhsText2,
6269
+ variable: right2.type === "identifier" ? rhsText2 : null,
6270
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
6271
+ value: null
6272
+ }],
6273
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6274
+ in_method: findEnclosingMethod(asn)
6275
+ });
6276
+ continue;
6277
+ }
6233
6278
  if (left?.type !== "member_access_expression") continue;
6234
6279
  const nameNode = left.childForFieldName("name");
6235
6280
  if (!nameNode) continue;
@@ -6239,6 +6284,14 @@ function extractCSharpCalls(tree, cache) {
6239
6284
  const recv = exprNode ? getNodeText(exprNode) : null;
6240
6285
  const recvType = recv ? typeMap.get(recv) : void 0;
6241
6286
  if (recvType !== "DirectorySearcher") continue;
6287
+ } else if (propName === "StatusDescription") {
6288
+ const recv = exprNode ? getNodeText(exprNode) : "";
6289
+ const recvType = recv ? typeMap.get(recv) : void 0;
6290
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv) && !(recvType && CSHARP_RESPONSE_TYPES.has(recvType))) continue;
6291
+ } else if (propName === "BaseAddress") {
6292
+ const recv = exprNode ? getNodeText(exprNode) : null;
6293
+ const recvType = recv ? typeMap.get(recv) : void 0;
6294
+ if (recvType !== "HttpClient") continue;
6242
6295
  } else if (propName !== "CommandText") {
6243
6296
  continue;
6244
6297
  }
@@ -6261,8 +6314,34 @@ function extractCSharpCalls(tree, cache) {
6261
6314
  in_method: findEnclosingMethod(asn)
6262
6315
  });
6263
6316
  }
6317
+ const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
6318
+ for (const cast of casts) {
6319
+ const typeNode = cast.childForFieldName("type");
6320
+ const valueNode = cast.childForFieldName("value");
6321
+ if (!typeNode || !valueNode) continue;
6322
+ const typeName = csharpBareName(typeNode);
6323
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
6324
+ const valueText = getNodeText(valueNode);
6325
+ calls.push({
6326
+ method_name: typeName,
6327
+ receiver: null,
6328
+ receiver_type: null,
6329
+ receiver_type_fqn: null,
6330
+ arguments: [{
6331
+ position: 0,
6332
+ expression: valueText,
6333
+ variable: valueNode.type === "identifier" ? valueText : null,
6334
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
6335
+ value: null
6336
+ }],
6337
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
6338
+ in_method: findEnclosingMethod(cast),
6339
+ is_constructor: true
6340
+ });
6341
+ }
6264
6342
  return calls;
6265
6343
  }
6344
+ var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
6266
6345
  function extractCSharpArguments(argsNode) {
6267
6346
  const args2 = [];
6268
6347
  let position = 0;
@@ -6282,12 +6361,22 @@ function extractCSharpArguments(argsNode) {
6282
6361
  position: position++,
6283
6362
  expression: text,
6284
6363
  variable: expr?.type === "identifier" ? text : null,
6285
- literal: expr?.type === "string_literal" ? text : null,
6364
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
6286
6365
  value: null
6287
6366
  });
6288
6367
  }
6289
6368
  return args2;
6290
6369
  }
6370
+ var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
6371
+ "string_literal",
6372
+ "verbatim_string_literal",
6373
+ "raw_string_literal",
6374
+ "character_literal",
6375
+ "integer_literal",
6376
+ "real_literal",
6377
+ "boolean_literal",
6378
+ "null_literal"
6379
+ ]);
6291
6380
  function extractJavaScriptCalls(tree, cache) {
6292
6381
  const calls = [];
6293
6382
  const context = buildJSResolutionContext(tree, cache);
@@ -11071,6 +11160,23 @@ var DEFAULT_SOURCES = [
11071
11160
  { method: "next", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11072
11161
  { method: "nextInt", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11073
11162
  // Database result sources
11163
+ // C# I/O, network and database reads (cognium-dev#502). Mirrors the Java
11164
+ // rows above one-for-one, including the severity split: console/file I/O is
11165
+ // `io_input` at high, a database row read is `db_input` at medium. There is
11166
+ // no separate confidence tier — `type` + `severity` IS the tier, exactly as
11167
+ // for `BufferedReader.readLine` vs `ResultSet.getString`.
11168
+ { method: "ReadLine", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11169
+ { method: "ReadLineAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11170
+ { method: "ReadToEnd", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11171
+ { method: "ReadToEndAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11172
+ // Socket and HTTP-client reads. `GetStream` / `AcceptTcpClient` /
11173
+ // `OpenRead` return the channel; the read off it is covered by the
11174
+ // StreamReader rows above, and seeding the channel too lets a flow form when
11175
+ // the read is inlined.
11176
+ { method: "GetStream", class: "TcpClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11177
+ { method: "AcceptTcpClient", class: "TcpListener", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11178
+ { method: "OpenRead", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11179
+ { method: "DownloadString", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11074
11180
  { method: "getString", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11075
11181
  { method: "getObject", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11076
11182
  { method: "getInt", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
@@ -13311,9 +13417,14 @@ var DEFAULT_SINKS = [
13311
13417
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13312
13418
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13313
13419
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13314
- // Generic deserialization patterns
13315
- { method: "from_str", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13316
- { method: "from_slice", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13420
+ // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13421
+ // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13422
+ // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
13423
+ // parsing, which cannot instantiate an attacker-chosen type. Removing them
13424
+ // was previously blocked because the class-scoped rows below could not match
13425
+ // the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
13426
+ // `::` path-separator fix in taint-matcher.ts makes them match, so the
13427
+ // classless rows are no longer load-bearing.
13317
13428
  // Rust XSS (actix-web, rocket, axum response body)
13318
13429
  { method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
13319
13430
  { method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -13630,8 +13741,12 @@ var DEFAULT_SINKS = [
13630
13741
  { method: "OpenRead", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13631
13742
  { method: "OpenWrite", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13632
13743
  { method: "FileStream", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13633
- { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13634
- { method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13744
+ // #502: the `(Stream)` overload takes no path — `new StreamReader(tcp.GetStream())`
13745
+ // is not CWE-22. The `(string path)` overload still matches.
13746
+ { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
13747
+ // #502: the `(Stream)` overload takes no path — `new StreamWriter(tcp.GetStream())`
13748
+ // is not CWE-22. The `(string path)` overload still matches.
13749
+ { method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
13635
13750
  // Remaining System.IO.File path APIs — class-scoped so they don't collide
13636
13751
  // with unrelated methods. Copy/Move take a source AND destination path (both
13637
13752
  // attacker-controllable → read-anywhere / write-anywhere), hence [0, 1].
@@ -13675,6 +13790,23 @@ var DEFAULT_SINKS = [
13675
13790
  // `new RestClient(userUrl)` points every later request at attacker-controlled
13676
13791
  // infrastructure. Ctor arg 0, taint-gated.
13677
13792
  { method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13793
+ // C# code injection — CodeDOM runtime compilation (CWE-94), cognium-dev#503
13794
+ // part 2. `provider.CompileAssemblyFromSource(parameters, source)` compiles
13795
+ // and can then execute attacker-supplied C#, so the source argument is the
13796
+ // injection point. The signature is
13797
+ // `CompileAssemblyFromSource(CompilerParameters, params string[] sources)`,
13798
+ // so the sources begin at position 1 — position 0 is the options object.
13799
+ //
13800
+ // These rows are only useful together with the #530 StringBuilder
13801
+ // propagation fix: the Juliet CWE-94 corpus accumulates the compiled source
13802
+ // through `sb.Append(...)` and reads it back with `ToString()`, so before
13803
+ // that fix the sink registered but no taint ever reached it.
13804
+ { method: "CompileAssemblyFromSource", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
13805
+ { method: "CompileAssemblyFromSourceBatch", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
13806
+ // Roslyn's equivalent pair. `ParseText` turns text into a syntax tree and
13807
+ // `Create` compiles it; either is the point where attacker text becomes code.
13808
+ { method: "ParseText", class: "CSharpSyntaxTree", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13809
+ { method: "ParseText", class: "SyntaxFactory", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13678
13810
  // C# code injection — dynamic script/assembly loading (CWE-94).
13679
13811
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13680
13812
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -13719,6 +13851,10 @@ var DEFAULT_SINKS = [
13719
13851
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13720
13852
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13721
13853
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13854
+ // #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
13855
+ // attacker text into the HTTP status line (CWE-81). Surfaced by the
13856
+ // property-assignment synthesiser in the C# extractor, receiver-gated there.
13857
+ { method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13722
13858
  // `return Content(html, "text/html")` on a controller writes arg 0 to the
13723
13859
  // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
13724
13860
  // `Content(constantString)` and the JSON/plain-text content types are
@@ -13733,6 +13869,12 @@ var DEFAULT_SINKS = [
13733
13869
  // filter is the constructor argument.
13734
13870
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13735
13871
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13872
+ // #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
13873
+ // host and port of every RELATIVE request made on that client, so the
13874
+ // request arguments can all be constant and the destination still be
13875
+ // attacker-chosen. Surfaced by the property-assignment synthesiser, which
13876
+ // class-gates it to a resolved `HttpClient`.
13877
+ { method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13736
13878
  // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
13737
13879
  // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
13738
13880
  // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
@@ -15446,6 +15588,20 @@ function escapeRe(s) {
15446
15588
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15447
15589
  }
15448
15590
  var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
15591
+ function argIsStreamExpression(call, position) {
15592
+ const arg = call.arguments.find((a) => a.position === position);
15593
+ if (!arg) return false;
15594
+ const expr = (arg.expression ?? "").trim();
15595
+ if (!expr) return false;
15596
+ return (
15597
+ // `new MemoryStream(...)`, `new FileStream(...)`, `new GZipStream(...)`
15598
+ /^new\s+[A-Za-z_][\w.]*Stream\s*\(/.test(expr) || // `tcp.GetStream()`, `resp.GetResponseStream()`, `file.OpenReadStream()`
15599
+ /\.\s*Get(?:Response)?Stream\s*\(/.test(expr) || /\.\s*OpenReadStream\s*\(/.test(expr) || // `wc.OpenRead(url)` returns a Stream (WebClient), as does `File.OpenRead`
15600
+ /\.\s*OpenRead\s*\(/.test(expr) || // `Request.Body`, `x.BaseStream`
15601
+ /\.\s*(?:Body|BaseStream)\s*$/.test(expr) || // a local whose name is stream-ish and is not a path string
15602
+ /^(?:[A-Za-z_]\w*\.)*[A-Za-z_]*[sS]tream\d*$/.test(expr)
15603
+ );
15604
+ }
15449
15605
  function argIsStringLiteral(call, position) {
15450
15606
  const arg = call.arguments.find((a) => a.position === position);
15451
15607
  if (!arg) return false;
@@ -15683,6 +15839,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
15683
15839
  if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
15684
15840
  continue;
15685
15841
  }
15842
+ if (pattern.safe_if_stream_arg_at !== void 0 && argIsStreamExpression(call, pattern.safe_if_stream_arg_at)) {
15843
+ continue;
15844
+ }
15686
15845
  if (pattern.type === "command_injection") {
15687
15846
  if (call.is_constructor) {
15688
15847
  if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
@@ -16077,8 +16236,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
16077
16236
  return false;
16078
16237
  } else if (!call.receiver && !call.receiver_type) {
16079
16238
  const target = call.resolution?.target;
16239
+ const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
16080
16240
  const expectedTail = `${pattern.class}.${pattern.method}`;
16081
- if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
16241
+ if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
16082
16242
  } else {
16083
16243
  return false;
16084
16244
  }
@@ -4414,6 +4414,21 @@ function extractTypes(tree, cache, language) {
4414
4414
  }
4415
4415
  return extractJavaTypes(tree, cache);
4416
4416
  }
4417
+ function csharpMemberNodes(body2) {
4418
+ if (!body2) return [];
4419
+ const out2 = [];
4420
+ const visit = (parent, depth) => {
4421
+ if (depth > 32) return;
4422
+ for (let i2 = 0; i2 < parent.childCount; i2++) {
4423
+ const c = parent.child(i2);
4424
+ if (!c) continue;
4425
+ if (c.type.startsWith("preproc_")) visit(c, depth + 1);
4426
+ else out2.push(c);
4427
+ }
4428
+ };
4429
+ visit(body2, 0);
4430
+ return out2;
4431
+ }
4417
4432
  function extractCSharpTypes(tree, cache) {
4418
4433
  const types = [];
4419
4434
  const KINDS = {
@@ -4430,9 +4445,8 @@ function extractCSharpTypes(tree, cache) {
4430
4445
  const methods = [];
4431
4446
  const fields = extractCSharpFields(body2);
4432
4447
  if (body2) {
4433
- for (let i2 = 0; i2 < body2.childCount; i2++) {
4434
- const m = body2.child(i2);
4435
- if (!m || m.type !== "method_declaration") continue;
4448
+ for (const m of csharpMemberNodes(body2)) {
4449
+ if (m.type !== "method_declaration") continue;
4436
4450
  const mName = m.childForFieldName("name");
4437
4451
  const paramList = m.childForFieldName("parameters");
4438
4452
  const parameters = [];
@@ -4483,9 +4497,7 @@ function extractCSharpTypes(tree, cache) {
4483
4497
  function extractCSharpFields(body2) {
4484
4498
  const fields = [];
4485
4499
  if (!body2) return fields;
4486
- for (let i2 = 0; i2 < body2.childCount; i2++) {
4487
- const c = body2.child(i2);
4488
- if (!c) continue;
4500
+ for (const c of csharpMemberNodes(body2)) {
4489
4501
  if (c.type === "field_declaration") {
4490
4502
  let varDecl = null;
4491
4503
  for (let k = 0; k < c.childCount; k++) {
@@ -6019,6 +6031,13 @@ function extractCalls(tree, cache, language) {
6019
6031
  function buildCSharpReceiverTypeMap(tree, cache) {
6020
6032
  const map = /* @__PURE__ */ new Map();
6021
6033
  const simple = (t) => t.replace(/<[^>]*>/g, "").split(".").pop()?.trim() ?? t;
6034
+ for (const prm of getNodesFromCache(tree.rootNode, "parameter", cache)) {
6035
+ const nameNode = prm.childForFieldName("name");
6036
+ const typeNode = prm.childForFieldName("type");
6037
+ if (nameNode?.type !== "identifier" || !typeNode) continue;
6038
+ const t = getNodeText(typeNode);
6039
+ if (t && t !== "var") map.set(getNodeText(nameNode), simple(t));
6040
+ }
6022
6041
  for (const vd of getNodesFromCache(tree.rootNode, "variable_declaration", cache)) {
6023
6042
  const typeNode = vd.childForFieldName("type");
6024
6043
  const declaredType = typeNode ? getNodeText(typeNode) : null;
@@ -6048,6 +6067,7 @@ function findFirstDescendant(node, type) {
6048
6067
  }
6049
6068
  return null;
6050
6069
  }
6070
+ var CSHARP_RESPONSE_TYPES = /* @__PURE__ */ new Set(["HttpResponse", "HttpResponseBase"]);
6051
6071
  var CSHARP_COMMAND_EXECUTE_METHODS = /* @__PURE__ */ new Set([
6052
6072
  "ExecuteScalar",
6053
6073
  "ExecuteReader",
@@ -6164,6 +6184,31 @@ function extractCSharpCalls(tree, cache) {
6164
6184
  });
6165
6185
  continue;
6166
6186
  }
6187
+ if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
6188
+ const creation = asn.parent.parent;
6189
+ if (creation?.type !== "object_creation_expression") continue;
6190
+ const ctorType = creation.childForFieldName("type");
6191
+ const right2 = asn.childForFieldName("right");
6192
+ if (!ctorType || !right2) continue;
6193
+ const typeName = csharpBareName(ctorType);
6194
+ const rhsText2 = getNodeText(right2);
6195
+ calls.push({
6196
+ method_name: getNodeText(left),
6197
+ receiver: typeName,
6198
+ receiver_type: typeName,
6199
+ receiver_type_fqn: null,
6200
+ arguments: [{
6201
+ position: 0,
6202
+ expression: rhsText2,
6203
+ variable: right2.type === "identifier" ? rhsText2 : null,
6204
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
6205
+ value: null
6206
+ }],
6207
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6208
+ in_method: findEnclosingMethod(asn)
6209
+ });
6210
+ continue;
6211
+ }
6167
6212
  if (left?.type !== "member_access_expression") continue;
6168
6213
  const nameNode = left.childForFieldName("name");
6169
6214
  if (!nameNode) continue;
@@ -6173,6 +6218,14 @@ function extractCSharpCalls(tree, cache) {
6173
6218
  const recv = exprNode ? getNodeText(exprNode) : null;
6174
6219
  const recvType = recv ? typeMap.get(recv) : void 0;
6175
6220
  if (recvType !== "DirectorySearcher") continue;
6221
+ } else if (propName === "StatusDescription") {
6222
+ const recv = exprNode ? getNodeText(exprNode) : "";
6223
+ const recvType = recv ? typeMap.get(recv) : void 0;
6224
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv) && !(recvType && CSHARP_RESPONSE_TYPES.has(recvType))) continue;
6225
+ } else if (propName === "BaseAddress") {
6226
+ const recv = exprNode ? getNodeText(exprNode) : null;
6227
+ const recvType = recv ? typeMap.get(recv) : void 0;
6228
+ if (recvType !== "HttpClient") continue;
6176
6229
  } else if (propName !== "CommandText") {
6177
6230
  continue;
6178
6231
  }
@@ -6195,8 +6248,34 @@ function extractCSharpCalls(tree, cache) {
6195
6248
  in_method: findEnclosingMethod(asn)
6196
6249
  });
6197
6250
  }
6251
+ const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
6252
+ for (const cast of casts) {
6253
+ const typeNode = cast.childForFieldName("type");
6254
+ const valueNode = cast.childForFieldName("value");
6255
+ if (!typeNode || !valueNode) continue;
6256
+ const typeName = csharpBareName(typeNode);
6257
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
6258
+ const valueText = getNodeText(valueNode);
6259
+ calls.push({
6260
+ method_name: typeName,
6261
+ receiver: null,
6262
+ receiver_type: null,
6263
+ receiver_type_fqn: null,
6264
+ arguments: [{
6265
+ position: 0,
6266
+ expression: valueText,
6267
+ variable: valueNode.type === "identifier" ? valueText : null,
6268
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
6269
+ value: null
6270
+ }],
6271
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
6272
+ in_method: findEnclosingMethod(cast),
6273
+ is_constructor: true
6274
+ });
6275
+ }
6198
6276
  return calls;
6199
6277
  }
6278
+ var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
6200
6279
  function extractCSharpArguments(argsNode) {
6201
6280
  const args2 = [];
6202
6281
  let position = 0;
@@ -6216,12 +6295,22 @@ function extractCSharpArguments(argsNode) {
6216
6295
  position: position++,
6217
6296
  expression: text,
6218
6297
  variable: expr?.type === "identifier" ? text : null,
6219
- literal: expr?.type === "string_literal" ? text : null,
6298
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
6220
6299
  value: null
6221
6300
  });
6222
6301
  }
6223
6302
  return args2;
6224
6303
  }
6304
+ var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
6305
+ "string_literal",
6306
+ "verbatim_string_literal",
6307
+ "raw_string_literal",
6308
+ "character_literal",
6309
+ "integer_literal",
6310
+ "real_literal",
6311
+ "boolean_literal",
6312
+ "null_literal"
6313
+ ]);
6225
6314
  function extractJavaScriptCalls(tree, cache) {
6226
6315
  const calls = [];
6227
6316
  const context = buildJSResolutionContext(tree, cache);
@@ -11005,6 +11094,23 @@ var DEFAULT_SOURCES = [
11005
11094
  { method: "next", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11006
11095
  { method: "nextInt", class: "Scanner", type: "io_input", severity: "high", return_tainted: true },
11007
11096
  // Database result sources
11097
+ // C# I/O, network and database reads (cognium-dev#502). Mirrors the Java
11098
+ // rows above one-for-one, including the severity split: console/file I/O is
11099
+ // `io_input` at high, a database row read is `db_input` at medium. There is
11100
+ // no separate confidence tier — `type` + `severity` IS the tier, exactly as
11101
+ // for `BufferedReader.readLine` vs `ResultSet.getString`.
11102
+ { method: "ReadLine", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11103
+ { method: "ReadLineAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11104
+ { method: "ReadToEnd", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11105
+ { method: "ReadToEndAsync", class: "StreamReader", type: "io_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11106
+ // Socket and HTTP-client reads. `GetStream` / `AcceptTcpClient` /
11107
+ // `OpenRead` return the channel; the read off it is covered by the
11108
+ // StreamReader rows above, and seeding the channel too lets a flow form when
11109
+ // the read is inlined.
11110
+ { method: "GetStream", class: "TcpClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11111
+ { method: "AcceptTcpClient", class: "TcpListener", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11112
+ { method: "OpenRead", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11113
+ { method: "DownloadString", class: "WebClient", type: "network_input", severity: "high", return_tainted: true, languages: ["csharp"] },
11008
11114
  { method: "getString", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11009
11115
  { method: "getObject", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
11010
11116
  { method: "getInt", class: "ResultSet", type: "db_input", severity: "medium", return_tainted: true },
@@ -13245,9 +13351,14 @@ var DEFAULT_SINKS = [
13245
13351
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13246
13352
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13247
13353
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13248
- // Generic deserialization patterns
13249
- { method: "from_str", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13250
- { method: "from_slice", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13354
+ // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13355
+ // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13356
+ // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
13357
+ // parsing, which cannot instantiate an attacker-chosen type. Removing them
13358
+ // was previously blocked because the class-scoped rows below could not match
13359
+ // the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
13360
+ // `::` path-separator fix in taint-matcher.ts makes them match, so the
13361
+ // classless rows are no longer load-bearing.
13251
13362
  // Rust XSS (actix-web, rocket, axum response body)
13252
13363
  { method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
13253
13364
  { method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -13564,8 +13675,12 @@ var DEFAULT_SINKS = [
13564
13675
  { method: "OpenRead", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13565
13676
  { method: "OpenWrite", class: "File", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13566
13677
  { method: "FileStream", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13567
- { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13568
- { method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["csharp"] },
13678
+ // #502: the `(Stream)` overload takes no path — `new StreamReader(tcp.GetStream())`
13679
+ // is not CWE-22. The `(string path)` overload still matches.
13680
+ { method: "StreamReader", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
13681
+ // #502: the `(Stream)` overload takes no path — `new StreamWriter(tcp.GetStream())`
13682
+ // is not CWE-22. The `(string path)` overload still matches.
13683
+ { method: "StreamWriter", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], safe_if_stream_arg_at: 0, languages: ["csharp"] },
13569
13684
  // Remaining System.IO.File path APIs — class-scoped so they don't collide
13570
13685
  // with unrelated methods. Copy/Move take a source AND destination path (both
13571
13686
  // attacker-controllable → read-anywhere / write-anywhere), hence [0, 1].
@@ -13609,6 +13724,23 @@ var DEFAULT_SINKS = [
13609
13724
  // `new RestClient(userUrl)` points every later request at attacker-controlled
13610
13725
  // infrastructure. Ctor arg 0, taint-gated.
13611
13726
  { method: "RestClient", class: "constructor", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13727
+ // C# code injection — CodeDOM runtime compilation (CWE-94), cognium-dev#503
13728
+ // part 2. `provider.CompileAssemblyFromSource(parameters, source)` compiles
13729
+ // and can then execute attacker-supplied C#, so the source argument is the
13730
+ // injection point. The signature is
13731
+ // `CompileAssemblyFromSource(CompilerParameters, params string[] sources)`,
13732
+ // so the sources begin at position 1 — position 0 is the options object.
13733
+ //
13734
+ // These rows are only useful together with the #530 StringBuilder
13735
+ // propagation fix: the Juliet CWE-94 corpus accumulates the compiled source
13736
+ // through `sb.Append(...)` and reads it back with `ToString()`, so before
13737
+ // that fix the sink registered but no taint ever reached it.
13738
+ { method: "CompileAssemblyFromSource", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
13739
+ { method: "CompileAssemblyFromSourceBatch", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [1, 2], languages: ["csharp"] },
13740
+ // Roslyn's equivalent pair. `ParseText` turns text into a syntax tree and
13741
+ // `Create` compiles it; either is the point where attacker text becomes code.
13742
+ { method: "ParseText", class: "CSharpSyntaxTree", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13743
+ { method: "ParseText", class: "SyntaxFactory", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13612
13744
  // C# code injection — dynamic script/assembly loading (CWE-94).
13613
13745
  { method: "EvaluateAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
13614
13746
  { method: "RunAsync", class: "CSharpScript", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
@@ -13653,6 +13785,10 @@ var DEFAULT_SINKS = [
13653
13785
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13654
13786
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13655
13787
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13788
+ // #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
13789
+ // attacker text into the HTTP status line (CWE-81). Surfaced by the
13790
+ // property-assignment synthesiser in the C# extractor, receiver-gated there.
13791
+ { method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13656
13792
  // `return Content(html, "text/html")` on a controller writes arg 0 to the
13657
13793
  // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
13658
13794
  // `Content(constantString)` and the JSON/plain-text content types are
@@ -13667,6 +13803,12 @@ var DEFAULT_SINKS = [
13667
13803
  // filter is the constructor argument.
13668
13804
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13669
13805
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13806
+ // #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
13807
+ // host and port of every RELATIVE request made on that client, so the
13808
+ // request arguments can all be constant and the destination still be
13809
+ // attacker-chosen. Surfaced by the property-assignment synthesiser, which
13810
+ // class-gates it to a resolved `HttpClient`.
13811
+ { method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13670
13812
  // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
13671
13813
  // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
13672
13814
  // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
@@ -15380,6 +15522,20 @@ function escapeRe(s) {
15380
15522
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15381
15523
  }
15382
15524
  var TYPE_TOKEN_RE = /^new\s+(?:TypeReference|TypeToken)\s*<[\s\S]*>\s*\(\s*\)\s*\{\s*\}$/;
15525
+ function argIsStreamExpression(call, position) {
15526
+ const arg = call.arguments.find((a) => a.position === position);
15527
+ if (!arg) return false;
15528
+ const expr = (arg.expression ?? "").trim();
15529
+ if (!expr) return false;
15530
+ return (
15531
+ // `new MemoryStream(...)`, `new FileStream(...)`, `new GZipStream(...)`
15532
+ /^new\s+[A-Za-z_][\w.]*Stream\s*\(/.test(expr) || // `tcp.GetStream()`, `resp.GetResponseStream()`, `file.OpenReadStream()`
15533
+ /\.\s*Get(?:Response)?Stream\s*\(/.test(expr) || /\.\s*OpenReadStream\s*\(/.test(expr) || // `wc.OpenRead(url)` returns a Stream (WebClient), as does `File.OpenRead`
15534
+ /\.\s*OpenRead\s*\(/.test(expr) || // `Request.Body`, `x.BaseStream`
15535
+ /\.\s*(?:Body|BaseStream)\s*$/.test(expr) || // a local whose name is stream-ish and is not a path string
15536
+ /^(?:[A-Za-z_]\w*\.)*[A-Za-z_]*[sS]tream\d*$/.test(expr)
15537
+ );
15538
+ }
15383
15539
  function argIsStringLiteral(call, position) {
15384
15540
  const arg = call.arguments.find((a) => a.position === position);
15385
15541
  if (!arg) return false;
@@ -15617,6 +15773,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
15617
15773
  if (pattern.safe_if_string_literal_at !== void 0 && argIsStringLiteral(call, pattern.safe_if_string_literal_at)) {
15618
15774
  continue;
15619
15775
  }
15776
+ if (pattern.safe_if_stream_arg_at !== void 0 && argIsStreamExpression(call, pattern.safe_if_stream_arg_at)) {
15777
+ continue;
15778
+ }
15620
15779
  if (pattern.type === "command_injection") {
15621
15780
  if (call.is_constructor) {
15622
15781
  if (!CWE_78_RECEIVER_ALLOWLIST.has(call.method_name)) {
@@ -16011,8 +16170,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
16011
16170
  return false;
16012
16171
  } else if (!call.receiver && !call.receiver_type) {
16013
16172
  const target = call.resolution?.target;
16173
+ const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
16014
16174
  const expectedTail = `${pattern.class}.${pattern.method}`;
16015
- if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
16175
+ if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
16016
16176
  } else {
16017
16177
  return false;
16018
16178
  }