circle-ir 4.9.27 → 4.9.28

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.
@@ -6184,6 +6184,31 @@ function extractCSharpCalls(tree, cache) {
6184
6184
  });
6185
6185
  continue;
6186
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
+ }
6187
6212
  if (left?.type !== "member_access_expression") continue;
6188
6213
  const nameNode = left.childForFieldName("name");
6189
6214
  if (!nameNode) continue;
@@ -6193,6 +6218,13 @@ function extractCSharpCalls(tree, cache) {
6193
6218
  const recv = exprNode ? getNodeText(exprNode) : null;
6194
6219
  const recvType = recv ? typeMap.get(recv) : void 0;
6195
6220
  if (recvType !== "DirectorySearcher") continue;
6221
+ } else if (propName === "StatusDescription") {
6222
+ const recv = exprNode ? getNodeText(exprNode) : "";
6223
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
6224
+ } else if (propName === "BaseAddress") {
6225
+ const recv = exprNode ? getNodeText(exprNode) : null;
6226
+ const recvType = recv ? typeMap.get(recv) : void 0;
6227
+ if (recvType !== "HttpClient") continue;
6196
6228
  } else if (propName !== "CommandText") {
6197
6229
  continue;
6198
6230
  }
@@ -6215,8 +6247,34 @@ function extractCSharpCalls(tree, cache) {
6215
6247
  in_method: findEnclosingMethod(asn)
6216
6248
  });
6217
6249
  }
6250
+ const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
6251
+ for (const cast of casts) {
6252
+ const typeNode = cast.childForFieldName("type");
6253
+ const valueNode = cast.childForFieldName("value");
6254
+ if (!typeNode || !valueNode) continue;
6255
+ const typeName = csharpBareName(typeNode);
6256
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
6257
+ const valueText = getNodeText(valueNode);
6258
+ calls.push({
6259
+ method_name: typeName,
6260
+ receiver: null,
6261
+ receiver_type: null,
6262
+ receiver_type_fqn: null,
6263
+ arguments: [{
6264
+ position: 0,
6265
+ expression: valueText,
6266
+ variable: valueNode.type === "identifier" ? valueText : null,
6267
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
6268
+ value: null
6269
+ }],
6270
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
6271
+ in_method: findEnclosingMethod(cast),
6272
+ is_constructor: true
6273
+ });
6274
+ }
6218
6275
  return calls;
6219
6276
  }
6277
+ var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
6220
6278
  function extractCSharpArguments(argsNode) {
6221
6279
  const args2 = [];
6222
6280
  let position = 0;
@@ -6236,12 +6294,22 @@ function extractCSharpArguments(argsNode) {
6236
6294
  position: position++,
6237
6295
  expression: text,
6238
6296
  variable: expr?.type === "identifier" ? text : null,
6239
- literal: expr?.type === "string_literal" ? text : null,
6297
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
6240
6298
  value: null
6241
6299
  });
6242
6300
  }
6243
6301
  return args2;
6244
6302
  }
6303
+ var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
6304
+ "string_literal",
6305
+ "verbatim_string_literal",
6306
+ "raw_string_literal",
6307
+ "character_literal",
6308
+ "integer_literal",
6309
+ "real_literal",
6310
+ "boolean_literal",
6311
+ "null_literal"
6312
+ ]);
6245
6313
  function extractJavaScriptCalls(tree, cache) {
6246
6314
  const calls = [];
6247
6315
  const context = buildJSResolutionContext(tree, cache);
@@ -13892,9 +13960,14 @@ var DEFAULT_SINKS = [
13892
13960
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13893
13961
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13894
13962
  { method: "from_str", class: "ron", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13895
- // Generic deserialization patterns
13896
- { method: "from_str", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13897
- { method: "from_slice", type: "deserialization", cwe: "CWE-502", severity: "medium", arg_positions: [0] },
13963
+ // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13964
+ // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13965
+ // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
13966
+ // parsing, which cannot instantiate an attacker-chosen type. Removing them
13967
+ // was previously blocked because the class-scoped rows below could not match
13968
+ // the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
13969
+ // `::` path-separator fix in taint-matcher.ts makes them match, so the
13970
+ // classless rows are no longer load-bearing.
13898
13971
  // Rust XSS (actix-web, rocket, axum response body)
13899
13972
  { method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
13900
13973
  { method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -14300,6 +14373,10 @@ var DEFAULT_SINKS = [
14300
14373
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
14301
14374
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
14302
14375
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
14376
+ // #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
14377
+ // attacker text into the HTTP status line (CWE-81). Surfaced by the
14378
+ // property-assignment synthesiser in the C# extractor, receiver-gated there.
14379
+ { method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
14303
14380
  // `return Content(html, "text/html")` on a controller writes arg 0 to the
14304
14381
  // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
14305
14382
  // `Content(constantString)` and the JSON/plain-text content types are
@@ -14314,6 +14391,12 @@ var DEFAULT_SINKS = [
14314
14391
  // filter is the constructor argument.
14315
14392
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
14316
14393
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
14394
+ // #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
14395
+ // host and port of every RELATIVE request made on that client, so the
14396
+ // request arguments can all be constant and the destination still be
14397
+ // attacker-chosen. Surfaced by the property-assignment synthesiser, which
14398
+ // class-gates it to a resolved `HttpClient`.
14399
+ { method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
14317
14400
  // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
14318
14401
  // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
14319
14402
  // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
@@ -16777,8 +16860,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
16777
16860
  return false;
16778
16861
  } else if (!call.receiver && !call.receiver_type) {
16779
16862
  const target = call.resolution?.target;
16863
+ const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
16780
16864
  const expectedTail = `${pattern.class}.${pattern.method}`;
16781
- if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
16865
+ if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
16782
16866
  } else {
16783
16867
  return false;
16784
16868
  }
@@ -19345,6 +19429,122 @@ var GO_PURE_PACKAGES = /* @__PURE__ */ new Set([
19345
19429
  "hex",
19346
19430
  "base64"
19347
19431
  ]);
19432
+ var CSHARP_PURE_STATIC_RECEIVERS = /* @__PURE__ */ new Set([
19433
+ // formatting / conversion / parsing — return a derived value
19434
+ "string",
19435
+ "String",
19436
+ "Convert",
19437
+ "Encoding",
19438
+ "BitConverter",
19439
+ "Math",
19440
+ "Int16",
19441
+ "Int32",
19442
+ "Int64",
19443
+ "UInt16",
19444
+ "UInt32",
19445
+ "UInt64",
19446
+ "Double",
19447
+ "Single",
19448
+ "Decimal",
19449
+ "Boolean",
19450
+ "Byte",
19451
+ "SByte",
19452
+ "Char",
19453
+ "Guid",
19454
+ "DateTime",
19455
+ "DateTimeOffset",
19456
+ "TimeSpan",
19457
+ "Enum",
19458
+ "Array",
19459
+ "Buffer",
19460
+ "Nullable",
19461
+ "CultureInfo",
19462
+ "StringComparer",
19463
+ "Comparer",
19464
+ "EqualityComparer",
19465
+ // logging / diagnostics receivers
19466
+ "Console",
19467
+ "Debug",
19468
+ "Trace",
19469
+ "Logger",
19470
+ "logger",
19471
+ "_logger",
19472
+ "Log"
19473
+ ]);
19474
+ var CSHARP_NON_ESCAPE_METHODS = /* @__PURE__ */ new Set([
19475
+ // string / value derivation
19476
+ "ToString",
19477
+ "ToArray",
19478
+ "ToList",
19479
+ "ToCharArray",
19480
+ "GetHashCode",
19481
+ "Equals",
19482
+ "CompareTo",
19483
+ "Trim",
19484
+ "TrimStart",
19485
+ "TrimEnd",
19486
+ "Substring",
19487
+ "Split",
19488
+ "Join",
19489
+ "Concat",
19490
+ "PadLeft",
19491
+ "PadRight",
19492
+ "ToLower",
19493
+ "ToUpper",
19494
+ "ToLowerInvariant",
19495
+ "ToUpperInvariant",
19496
+ "StartsWith",
19497
+ "EndsWith",
19498
+ "Contains",
19499
+ "IndexOf",
19500
+ "LastIndexOf",
19501
+ "Insert",
19502
+ "Remove",
19503
+ "Normalize",
19504
+ "Append",
19505
+ "AppendLine",
19506
+ "AppendFormat",
19507
+ "TryParse",
19508
+ "Parse",
19509
+ // structured logging (Microsoft.Extensions.Logging, Serilog, NLog, log4net)
19510
+ "LogInformation",
19511
+ "LogWarning",
19512
+ "LogError",
19513
+ "LogDebug",
19514
+ "LogTrace",
19515
+ "LogCritical",
19516
+ "Information",
19517
+ "Warning",
19518
+ "Verbose",
19519
+ "Fatal",
19520
+ "BeginScope",
19521
+ // Collection stores. A value put into a container has not left the program —
19522
+ // the same reasoning that puts Go's `append` in GO_BUILTIN_CALLS. Measured
19523
+ // on the 10 scored Juliet C# families these were 1,296 of the 2,368 escape
19524
+ // rows remaining after the first pass of this gate (#474): `Add` 864,
19525
+ // `AddLast` 432.
19526
+ //
19527
+ // Project methods are deliberately NOT gated, matching the Go gate's rule
19528
+ // that an unknown call must still escape: Juliet's own `BadSink` /
19529
+ // `GoodB2GSink` / `Action` dispatches account for a further 840 rows and are
19530
+ // left alone, because "the callee is defined in this file" is a different
19531
+ // claim needing its own change.
19532
+ "Add",
19533
+ "AddLast",
19534
+ "AddFirst",
19535
+ "AddRange",
19536
+ "TryAdd",
19537
+ "Insert",
19538
+ "Push",
19539
+ "Enqueue"
19540
+ ]);
19541
+ function isCSharpNonEscapeCall(call) {
19542
+ const name2 = call.method_name;
19543
+ const recv = (call.receiver ?? "").split(".").pop() ?? "";
19544
+ if (CSHARP_PURE_STATIC_RECEIVERS.has(recv)) return true;
19545
+ if (CSHARP_NON_ESCAPE_METHODS.has(name2)) return true;
19546
+ return false;
19547
+ }
19348
19548
  function isGoNonEscapeCall(call) {
19349
19549
  const name2 = call.method_name;
19350
19550
  const recv = (call.receiver ?? "").split(".").pop() ?? "";
@@ -19611,6 +19811,9 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
19611
19811
  if (graph.ir.meta.language === "go" && isGoNonEscapeCall(call)) {
19612
19812
  continue;
19613
19813
  }
19814
+ if (graph.ir.meta.language === "csharp" && isCSharpNonEscapeCall(call)) {
19815
+ continue;
19816
+ }
19614
19817
  const bashSafeBuiltins = /* @__PURE__ */ new Set([
19615
19818
  "echo",
19616
19819
  "printf",
@@ -27786,7 +27989,8 @@ var LanguageSourcesPass = class {
27786
27989
  }
27787
27990
  if (language === "csharp") {
27788
27991
  additionalSanitizers.push(
27789
- ...findCSharpFullPathStartsWithGuardSanitizers(code)
27992
+ ...findCSharpFullPathStartsWithGuardSanitizers(code),
27993
+ ...findCSharpEarlyReturnGuardSanitizers(code, language)
27790
27994
  );
27791
27995
  }
27792
27996
  if (language === "java") {
@@ -30902,6 +31106,66 @@ function guardRejects(lines, i2, terminatorRe, window2) {
30902
31106
  }
30903
31107
  return false;
30904
31108
  }
31109
+ function findCSharpEarlyReturnGuardSanitizers(code, language) {
31110
+ if (language !== "csharp") return [];
31111
+ const sanitizers = [];
31112
+ const lines = code.split("\n");
31113
+ const terminatorRe = /\b(throw|return)\b/;
31114
+ const allowlistRes = [
31115
+ /\bif\s*\(\s*Array\s*\.\s*IndexOf\s*\([^,]+,\s*([A-Za-z_][\w.]*)\s*\)\s*<\s*0\s*\)/,
31116
+ /\bif\s*\(\s*!\s*[A-Za-z_][\w.]*\s*\.\s*Contains\s*\(\s*([A-Za-z_][\w.]*)\s*\)\s*\)/
31117
+ ];
31118
+ const charClassRe = /\bif\s*\(\s*!\s*([A-Za-z_][\w.]*)\s*\.\s*All\s*\(\s*char\s*\.\s*Is[A-Za-z]+\s*\)\s*\)/;
31119
+ for (let i2 = 0; i2 < lines.length; i2++) {
31120
+ let guardedExpr = null;
31121
+ for (const re of allowlistRes) {
31122
+ const m = re.exec(lines[i2]);
31123
+ if (m) {
31124
+ guardedExpr = m[1];
31125
+ break;
31126
+ }
31127
+ }
31128
+ if (!guardedExpr) {
31129
+ const m = charClassRe.exec(lines[i2]);
31130
+ if (m) guardedExpr = m[1];
31131
+ }
31132
+ if (!guardedExpr) continue;
31133
+ if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
31134
+ const root = guardedExpr.split(".")[0];
31135
+ const guarded = /* @__PURE__ */ new Set([guardedExpr, root]);
31136
+ const assignRe = /^\s*(?:(?:var|string)\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
31137
+ for (let b = i2 - 1; b >= 0; b--) {
31138
+ const a = assignRe.exec(lines[b]);
31139
+ if (!a || !guarded.has(a[1])) continue;
31140
+ for (const id of a[2].match(/[A-Za-z_]\w*/g) ?? []) guarded.add(id);
31141
+ }
31142
+ const mentions = (t) => {
31143
+ for (const n of guarded) if (new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(t)) return true;
31144
+ return false;
31145
+ };
31146
+ for (let l = i2 + 1; l < lines.length; l++) {
31147
+ const a = assignRe.exec(lines[l]);
31148
+ if (a && mentions(a[2])) guarded.add(a[1]);
31149
+ if (mentions(lines[l])) {
31150
+ sanitizers.push({
31151
+ type: "csharp_early_return_guard",
31152
+ method: "guard",
31153
+ line: l + 1,
31154
+ sanitizes: [
31155
+ "ssrf",
31156
+ "xpath_injection",
31157
+ "ldap_injection",
31158
+ "path_traversal",
31159
+ "command_injection",
31160
+ "sql_injection",
31161
+ "external_taint_escape"
31162
+ ]
31163
+ });
31164
+ }
31165
+ }
31166
+ }
31167
+ return sanitizers;
31168
+ }
30905
31169
  function findCSharpFullPathStartsWithGuardSanitizers(code) {
30906
31170
  const sanitizers = [];
30907
31171
  const lines = code.split("\n");
@@ -37299,6 +37563,53 @@ var LibraryProfileXssGatePass = class {
37299
37563
  };
37300
37564
 
37301
37565
  // src/analysis/passes/taint-propagation-pass.ts
37566
+ var CONTAINER_READ_SOURCES = /* @__PURE__ */ new Set(["plugin_param", "config_param"]);
37567
+ function makeConstPropVeto(constProp, graph, sources, code) {
37568
+ const lines = typeof code === "string" ? code.split("\n") : [];
37569
+ const selfReassign = /* @__PURE__ */ new Map();
37570
+ const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37571
+ for (const [line, defs] of graph.defsByLine) {
37572
+ const text = lines[line - 1];
37573
+ if (!text) continue;
37574
+ for (const d of defs) {
37575
+ if (d.kind !== "local" && d.kind !== "param") continue;
37576
+ const v = escape(d.variable);
37577
+ const m = new RegExp(`(?:^|[^\\w.$])${v}\\s*(?:([-+*/%&|^]|<<|>>)=|=(?!=))(.*)$`).exec(text);
37578
+ if (!m) continue;
37579
+ if (m[1] || new RegExp(`(?:^|[^\\w.$])${v}\\b`).test(m[2])) {
37580
+ const arr = selfReassign.get(d.variable) ?? [];
37581
+ arr.push(line);
37582
+ selfReassign.set(d.variable, arr);
37583
+ }
37584
+ }
37585
+ }
37586
+ const reassignedAt = (v, line) => (selfReassign.get(v) ?? []).includes(line);
37587
+ const defAt = (v, line) => {
37588
+ const own = (graph.defsByLine.get(line) ?? []).find((d) => d.variable === v);
37589
+ if (own) return own;
37590
+ const use = (graph.usesByLine.get(line) ?? []).find((u) => u.variable === v && u.def_id != null);
37591
+ return use ? graph.defById.get(use.def_id) : void 0;
37592
+ };
37593
+ return (step, sourceLine, sinkLine) => {
37594
+ const r = isFalsePositive(constProp, step.line, step.variable);
37595
+ if (!r.isFalsePositive) return false;
37596
+ if (r.reason !== "variable_not_tainted") return true;
37597
+ const lo = Math.min(sourceLine, sinkLine);
37598
+ let d = defAt(step.variable, step.line);
37599
+ let selfHops = 0;
37600
+ for (let guard = 0; d && d.line > lo && reassignedAt(d.variable, d.line) && guard < 32; guard++) {
37601
+ const here = d.id;
37602
+ const prior = (graph.usesByLine.get(d.line) ?? []).find((u) => u.variable === d.variable && u.def_id != null && u.def_id !== here);
37603
+ if (!prior) break;
37604
+ selfHops++;
37605
+ d = graph.defById.get(prior.def_id);
37606
+ }
37607
+ if (!d) return true;
37608
+ if (d.line === sourceLine && sources.some((s) => s.line === sourceLine && !CONTAINER_READ_SOURCES.has(s.type))) return false;
37609
+ if (selfHops > 0 && d.line >= lo) return false;
37610
+ return true;
37611
+ };
37612
+ }
37302
37613
  var TaintPropagationPass = class {
37303
37614
  name = "taint-propagation";
37304
37615
  category = "security";
@@ -37316,11 +37627,11 @@ var TaintPropagationPass = class {
37316
37627
  return { flows: [] };
37317
37628
  }
37318
37629
  const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
37630
+ const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
37319
37631
  const verifiedFlows = propagationResult.flows.filter((flow) => {
37320
37632
  if (constProp.unreachableLines.has(flow.sink.line)) return false;
37321
37633
  for (const step of flow.path) {
37322
- const fpCheck = isFalsePositive(constProp, step.line, step.variable);
37323
- if (fpCheck.isFalsePositive) return false;
37634
+ if (isVetoed(step, flow.source.line, flow.sink.line)) return false;
37324
37635
  }
37325
37636
  if (isCorrelatedPredicateFP(constProp, flow)) return false;
37326
37637
  return true;
@@ -37413,7 +37724,7 @@ var TaintPropagationPass = class {
37413
37724
  if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
37414
37725
  let isFP = false;
37415
37726
  for (const step of f.path) {
37416
- if (isFalsePositive(constProp, step.line, step.variable).isFalsePositive) {
37727
+ if (isVetoed(step, f.source_line, f.sink_line)) {
37417
37728
  isFP = true;
37418
37729
  break;
37419
37730
  }
@@ -37436,7 +37747,7 @@ var TaintPropagationPass = class {
37436
37747
  if (isCorrelatedPredicateFP(constProp, flowForCheck)) continue;
37437
37748
  let isFP = false;
37438
37749
  for (const step of f.path) {
37439
- if (isFalsePositive(constProp, step.line, step.variable).isFalsePositive) {
37750
+ if (isVetoed(step, f.source_line, f.sink_line)) {
37440
37751
  isFP = true;
37441
37752
  break;
37442
37753
  }
@@ -40009,6 +40320,7 @@ var VariableShadowingPass = class {
40009
40320
  const outerEntry = declLocals[0];
40010
40321
  for (let i2 = 1; i2 < numDeclLocals; i2++) {
40011
40322
  const inner = declLocals[i2];
40323
+ if (inner.def.line <= outerEntry.def.line) continue;
40012
40324
  if (!isInNestedScope(codeLines, outerEntry.def.line, inner.def.line)) continue;
40013
40325
  const key = `${variable}-${inner.def.line}`;
40014
40326
  if (reported.has(key)) continue;
@@ -6230,6 +6230,31 @@ function extractCSharpCalls(tree, cache) {
6230
6230
  });
6231
6231
  continue;
6232
6232
  }
6233
+ if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
6234
+ const creation = asn.parent.parent;
6235
+ if (creation?.type !== "object_creation_expression") continue;
6236
+ const ctorType = creation.childForFieldName("type");
6237
+ const right2 = asn.childForFieldName("right");
6238
+ if (!ctorType || !right2) continue;
6239
+ const typeName = csharpBareName(ctorType);
6240
+ const rhsText2 = getNodeText(right2);
6241
+ calls.push({
6242
+ method_name: getNodeText(left),
6243
+ receiver: typeName,
6244
+ receiver_type: typeName,
6245
+ receiver_type_fqn: null,
6246
+ arguments: [{
6247
+ position: 0,
6248
+ expression: rhsText2,
6249
+ variable: right2.type === "identifier" ? rhsText2 : null,
6250
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
6251
+ value: null
6252
+ }],
6253
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6254
+ in_method: findEnclosingMethod(asn)
6255
+ });
6256
+ continue;
6257
+ }
6233
6258
  if (left?.type !== "member_access_expression") continue;
6234
6259
  const nameNode = left.childForFieldName("name");
6235
6260
  if (!nameNode) continue;
@@ -6239,6 +6264,13 @@ function extractCSharpCalls(tree, cache) {
6239
6264
  const recv = exprNode ? getNodeText(exprNode) : null;
6240
6265
  const recvType = recv ? typeMap.get(recv) : void 0;
6241
6266
  if (recvType !== "DirectorySearcher") continue;
6267
+ } else if (propName === "StatusDescription") {
6268
+ const recv = exprNode ? getNodeText(exprNode) : "";
6269
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
6270
+ } else if (propName === "BaseAddress") {
6271
+ const recv = exprNode ? getNodeText(exprNode) : null;
6272
+ const recvType = recv ? typeMap.get(recv) : void 0;
6273
+ if (recvType !== "HttpClient") continue;
6242
6274
  } else if (propName !== "CommandText") {
6243
6275
  continue;
6244
6276
  }
@@ -6261,8 +6293,34 @@ function extractCSharpCalls(tree, cache) {
6261
6293
  in_method: findEnclosingMethod(asn)
6262
6294
  });
6263
6295
  }
6296
+ const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
6297
+ for (const cast of casts) {
6298
+ const typeNode = cast.childForFieldName("type");
6299
+ const valueNode = cast.childForFieldName("value");
6300
+ if (!typeNode || !valueNode) continue;
6301
+ const typeName = csharpBareName(typeNode);
6302
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
6303
+ const valueText = getNodeText(valueNode);
6304
+ calls.push({
6305
+ method_name: typeName,
6306
+ receiver: null,
6307
+ receiver_type: null,
6308
+ receiver_type_fqn: null,
6309
+ arguments: [{
6310
+ position: 0,
6311
+ expression: valueText,
6312
+ variable: valueNode.type === "identifier" ? valueText : null,
6313
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
6314
+ value: null
6315
+ }],
6316
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
6317
+ in_method: findEnclosingMethod(cast),
6318
+ is_constructor: true
6319
+ });
6320
+ }
6264
6321
  return calls;
6265
6322
  }
6323
+ var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
6266
6324
  function extractCSharpArguments(argsNode) {
6267
6325
  const args2 = [];
6268
6326
  let position = 0;
@@ -6282,12 +6340,22 @@ function extractCSharpArguments(argsNode) {
6282
6340
  position: position++,
6283
6341
  expression: text,
6284
6342
  variable: expr?.type === "identifier" ? text : null,
6285
- literal: expr?.type === "string_literal" ? text : null,
6343
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
6286
6344
  value: null
6287
6345
  });
6288
6346
  }
6289
6347
  return args2;
6290
6348
  }
6349
+ var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
6350
+ "string_literal",
6351
+ "verbatim_string_literal",
6352
+ "raw_string_literal",
6353
+ "character_literal",
6354
+ "integer_literal",
6355
+ "real_literal",
6356
+ "boolean_literal",
6357
+ "null_literal"
6358
+ ]);
6291
6359
  function extractJavaScriptCalls(tree, cache) {
6292
6360
  const calls = [];
6293
6361
  const context = buildJSResolutionContext(tree, cache);
@@ -13311,9 +13379,14 @@ var DEFAULT_SINKS = [
13311
13379
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13312
13380
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13313
13381
  { 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] },
13382
+ // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13383
+ // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13384
+ // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
13385
+ // parsing, which cannot instantiate an attacker-chosen type. Removing them
13386
+ // was previously blocked because the class-scoped rows below could not match
13387
+ // the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
13388
+ // `::` path-separator fix in taint-matcher.ts makes them match, so the
13389
+ // classless rows are no longer load-bearing.
13317
13390
  // Rust XSS (actix-web, rocket, axum response body)
13318
13391
  { method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
13319
13392
  { method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -13719,6 +13792,10 @@ var DEFAULT_SINKS = [
13719
13792
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13720
13793
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13721
13794
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13795
+ // #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
13796
+ // attacker text into the HTTP status line (CWE-81). Surfaced by the
13797
+ // property-assignment synthesiser in the C# extractor, receiver-gated there.
13798
+ { method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13722
13799
  // `return Content(html, "text/html")` on a controller writes arg 0 to the
13723
13800
  // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
13724
13801
  // `Content(constantString)` and the JSON/plain-text content types are
@@ -13733,6 +13810,12 @@ var DEFAULT_SINKS = [
13733
13810
  // filter is the constructor argument.
13734
13811
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13735
13812
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13813
+ // #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
13814
+ // host and port of every RELATIVE request made on that client, so the
13815
+ // request arguments can all be constant and the destination still be
13816
+ // attacker-chosen. Surfaced by the property-assignment synthesiser, which
13817
+ // class-gates it to a resolved `HttpClient`.
13818
+ { method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13736
13819
  // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
13737
13820
  // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
13738
13821
  // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
@@ -16077,8 +16160,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
16077
16160
  return false;
16078
16161
  } else if (!call.receiver && !call.receiver_type) {
16079
16162
  const target = call.resolution?.target;
16163
+ const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
16080
16164
  const expectedTail = `${pattern.class}.${pattern.method}`;
16081
- if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
16165
+ if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
16082
16166
  } else {
16083
16167
  return false;
16084
16168
  }