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.
@@ -6164,6 +6164,31 @@ function extractCSharpCalls(tree, cache) {
6164
6164
  });
6165
6165
  continue;
6166
6166
  }
6167
+ if (left?.type === "identifier" && asn.parent?.type === "initializer_expression") {
6168
+ const creation = asn.parent.parent;
6169
+ if (creation?.type !== "object_creation_expression") continue;
6170
+ const ctorType = creation.childForFieldName("type");
6171
+ const right2 = asn.childForFieldName("right");
6172
+ if (!ctorType || !right2) continue;
6173
+ const typeName = csharpBareName(ctorType);
6174
+ const rhsText2 = getNodeText(right2);
6175
+ calls.push({
6176
+ method_name: getNodeText(left),
6177
+ receiver: typeName,
6178
+ receiver_type: typeName,
6179
+ receiver_type_fqn: null,
6180
+ arguments: [{
6181
+ position: 0,
6182
+ expression: rhsText2,
6183
+ variable: right2.type === "identifier" ? rhsText2 : null,
6184
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right2.type) ? rhsText2 : null,
6185
+ value: null
6186
+ }],
6187
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
6188
+ in_method: findEnclosingMethod(asn)
6189
+ });
6190
+ continue;
6191
+ }
6167
6192
  if (left?.type !== "member_access_expression") continue;
6168
6193
  const nameNode = left.childForFieldName("name");
6169
6194
  if (!nameNode) continue;
@@ -6173,6 +6198,13 @@ function extractCSharpCalls(tree, cache) {
6173
6198
  const recv = exprNode ? getNodeText(exprNode) : null;
6174
6199
  const recvType = recv ? typeMap.get(recv) : void 0;
6175
6200
  if (recvType !== "DirectorySearcher") continue;
6201
+ } else if (propName === "StatusDescription") {
6202
+ const recv = exprNode ? getNodeText(exprNode) : "";
6203
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv)) continue;
6204
+ } else if (propName === "BaseAddress") {
6205
+ const recv = exprNode ? getNodeText(exprNode) : null;
6206
+ const recvType = recv ? typeMap.get(recv) : void 0;
6207
+ if (recvType !== "HttpClient") continue;
6176
6208
  } else if (propName !== "CommandText") {
6177
6209
  continue;
6178
6210
  }
@@ -6195,8 +6227,34 @@ function extractCSharpCalls(tree, cache) {
6195
6227
  in_method: findEnclosingMethod(asn)
6196
6228
  });
6197
6229
  }
6230
+ const casts = getNodesFromCache(tree.rootNode, "cast_expression", cache);
6231
+ for (const cast of casts) {
6232
+ const typeNode = cast.childForFieldName("type");
6233
+ const valueNode = cast.childForFieldName("value");
6234
+ if (!typeNode || !valueNode) continue;
6235
+ const typeName = csharpBareName(typeNode);
6236
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName)) continue;
6237
+ const valueText = getNodeText(valueNode);
6238
+ calls.push({
6239
+ method_name: typeName,
6240
+ receiver: null,
6241
+ receiver_type: null,
6242
+ receiver_type_fqn: null,
6243
+ arguments: [{
6244
+ position: 0,
6245
+ expression: valueText,
6246
+ variable: valueNode.type === "identifier" ? valueText : null,
6247
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
6248
+ value: null
6249
+ }],
6250
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
6251
+ in_method: findEnclosingMethod(cast),
6252
+ is_constructor: true
6253
+ });
6254
+ }
6198
6255
  return calls;
6199
6256
  }
6257
+ var CSHARP_RAW_MARKUP_CAST_TYPES = /* @__PURE__ */ new Set(["MarkupString"]);
6200
6258
  function extractCSharpArguments(argsNode) {
6201
6259
  const args2 = [];
6202
6260
  let position = 0;
@@ -6216,12 +6274,22 @@ function extractCSharpArguments(argsNode) {
6216
6274
  position: position++,
6217
6275
  expression: text,
6218
6276
  variable: expr?.type === "identifier" ? text : null,
6219
- literal: expr?.type === "string_literal" ? text : null,
6277
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
6220
6278
  value: null
6221
6279
  });
6222
6280
  }
6223
6281
  return args2;
6224
6282
  }
6283
+ var CSHARP_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set([
6284
+ "string_literal",
6285
+ "verbatim_string_literal",
6286
+ "raw_string_literal",
6287
+ "character_literal",
6288
+ "integer_literal",
6289
+ "real_literal",
6290
+ "boolean_literal",
6291
+ "null_literal"
6292
+ ]);
6225
6293
  function extractJavaScriptCalls(tree, cache) {
6226
6294
  const calls = [];
6227
6295
  const context = buildJSResolutionContext(tree, cache);
@@ -13245,9 +13313,14 @@ var DEFAULT_SINKS = [
13245
13313
  { method: "deserialize", class: "bincode", type: "deserialization", cwe: "CWE-502", severity: "critical", arg_positions: [0] },
13246
13314
  { method: "from_str", class: "toml", type: "deserialization", cwe: "CWE-502", severity: "high", arg_positions: [0] },
13247
13315
  { 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] },
13316
+ // #294/#484: no classless `from_str` / `from_slice` deserialization sink.
13317
+ // Those rows matched EVERY `T::from_str` / `from_slice` — `u32::from_str`,
13318
+ // `Url::from_str`, `PublicKey::from_slice` — i.e. ordinary `FromStr` / byte
13319
+ // parsing, which cannot instantiate an attacker-chosen type. Removing them
13320
+ // was previously blocked because the class-scoped rows below could not match
13321
+ // the IMPORTED call form (`use serde_json::from_str; from_str(&body)`); the
13322
+ // `::` path-separator fix in taint-matcher.ts makes them match, so the
13323
+ // classless rows are no longer load-bearing.
13251
13324
  // Rust XSS (actix-web, rocket, axum response body)
13252
13325
  { method: "body", class: "HttpResponseBuilder", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
13253
13326
  { method: "body", class: "HttpResponse", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
@@ -13653,6 +13726,10 @@ var DEFAULT_SINKS = [
13653
13726
  // Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
13654
13727
  // documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
13655
13728
  { method: "MarkupString", class: "constructor", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
13729
+ // #503 (part 1): `Response.StatusDescription = "Bad " + data` writes
13730
+ // attacker text into the HTTP status line (CWE-81). Surfaced by the
13731
+ // property-assignment synthesiser in the C# extractor, receiver-gated there.
13732
+ { method: "StatusDescription", class: "Response", type: "xss", cwe: "CWE-81", severity: "medium", arg_positions: [0], languages: ["csharp"] },
13656
13733
  // `return Content(html, "text/html")` on a controller writes arg 0 to the
13657
13734
  // response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
13658
13735
  // `Content(constantString)` and the JSON/plain-text content types are
@@ -13667,6 +13744,12 @@ var DEFAULT_SINKS = [
13667
13744
  // filter is the constructor argument.
13668
13745
  { method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13669
13746
  { method: "DirectoryEntry", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
13747
+ // #336: a tainted `HttpClient.BaseAddress` gives the attacker the scheme,
13748
+ // host and port of every RELATIVE request made on that client, so the
13749
+ // request arguments can all be constant and the destination still be
13750
+ // attacker-chosen. Surfaced by the property-assignment synthesiser, which
13751
+ // class-gates it to a resolved `HttpClient`.
13752
+ { method: "BaseAddress", class: "HttpClient", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [0], languages: ["csharp"] },
13670
13753
  // C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
13671
13754
  // cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
13672
13755
  // an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
@@ -16011,8 +16094,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
16011
16094
  return false;
16012
16095
  } else if (!call.receiver && !call.receiver_type) {
16013
16096
  const target = call.resolution?.target;
16097
+ const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
16014
16098
  const expectedTail = `${pattern.class}.${pattern.method}`;
16015
- if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
16099
+ if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
16016
16100
  } else {
16017
16101
  return false;
16018
16102
  }
@@ -320,6 +320,44 @@ function extractCSharpCalls(tree, cache) {
320
320
  });
321
321
  continue;
322
322
  }
323
+ // cognium-dev#336 — inside an OBJECT INITIALIZER the assignment's left side
324
+ // is a bare identifier, not a member access:
325
+ //
326
+ // new HttpClient { BaseAddress = new Uri(input) }
327
+ // object_creation_expression
328
+ // initializer_expression
329
+ // assignment_expression left = identifier 'BaseAddress'
330
+ //
331
+ // so the member-access branch below skipped it entirely. Surface it with
332
+ // the CONSTRUCTED TYPE as the receiver, which is what the class-scoped
333
+ // registry row needs to match.
334
+ if (left?.type === 'identifier' && asn.parent?.type === 'initializer_expression') {
335
+ const creation = asn.parent.parent;
336
+ if (creation?.type !== 'object_creation_expression')
337
+ continue;
338
+ const ctorType = creation.childForFieldName('type');
339
+ const right = asn.childForFieldName('right');
340
+ if (!ctorType || !right)
341
+ continue;
342
+ const typeName = csharpBareName(ctorType);
343
+ const rhsText = getNodeText(right);
344
+ calls.push({
345
+ method_name: getNodeText(left),
346
+ receiver: typeName,
347
+ receiver_type: typeName,
348
+ receiver_type_fqn: null,
349
+ arguments: [{
350
+ position: 0,
351
+ expression: rhsText,
352
+ variable: right.type === 'identifier' ? rhsText : null,
353
+ literal: CSHARP_LITERAL_NODE_TYPES.has(right.type) ? rhsText : null,
354
+ value: null,
355
+ }],
356
+ location: { line: asn.startPosition.row + 1, column: asn.startPosition.column },
357
+ in_method: findEnclosingMethod(asn),
358
+ });
359
+ continue;
360
+ }
323
361
  if (left?.type !== 'member_access_expression')
324
362
  continue;
325
363
  const nameNode = left.childForFieldName('name');
@@ -337,6 +375,25 @@ function extractCSharpCalls(tree, cache) {
337
375
  if (recvType !== 'DirectorySearcher')
338
376
  continue;
339
377
  }
378
+ else if (propName === 'StatusDescription') {
379
+ // cognium-dev#503 (part 1) — `Response.StatusDescription = "Bad " + data`
380
+ // writes attacker text into the status line (CWE-81). Receiver-gated to
381
+ // a Response-shaped name so an unrelated `.StatusDescription =` on some
382
+ // other type is not surfaced.
383
+ const recv = exprNode ? getNodeText(exprNode) : '';
384
+ if (!/(^|\.)(Response|HttpResponse|Context\.Response)$/.test(recv))
385
+ continue;
386
+ }
387
+ else if (propName === 'BaseAddress') {
388
+ // cognium-dev#336 — `new HttpClient { BaseAddress = new Uri(input) }`
389
+ // hands the attacker the scheme, host and port of every relative request
390
+ // on that client (CWE-918). Class-gated to a resolved HttpClient so an
391
+ // unrelated `.BaseAddress =` is not surfaced.
392
+ const recv = exprNode ? getNodeText(exprNode) : null;
393
+ const recvType = recv ? typeMap.get(recv) : undefined;
394
+ if (recvType !== 'HttpClient')
395
+ continue;
396
+ }
340
397
  else if (propName !== 'CommandText') {
341
398
  continue;
342
399
  }
@@ -360,8 +417,51 @@ function extractCSharpCalls(tree, cache) {
360
417
  in_method: findEnclosingMethod(asn),
361
418
  });
362
419
  }
420
+ // cognium-dev#340 — a C# CAST is not a call node, so `(MarkupString)html`
421
+ // could not be reached by any registry row while `new MarkupString(html)`
422
+ // could. The two are interchangeable in Blazor and the cast is the more
423
+ // common of the pair, since `@((MarkupString)html)` in a `.razor` component
424
+ // compiles from it. Emitted as a constructor-shaped call so the EXISTING
425
+ // `{ method: 'MarkupString', class: 'constructor' }` row matches, which keeps
426
+ // the taint engine in charge: a constant operand carries no taint and so
427
+ // still reports nothing.
428
+ //
429
+ // Restricted to an allowlist rather than surfacing every cast: `(string)x`,
430
+ // `(int)x` and friends are ubiquitous and must not become synthetic calls.
431
+ const casts = getNodesFromCache(tree.rootNode, 'cast_expression', cache);
432
+ for (const cast of casts) {
433
+ const typeNode = cast.childForFieldName('type');
434
+ const valueNode = cast.childForFieldName('value');
435
+ if (!typeNode || !valueNode)
436
+ continue;
437
+ const typeName = csharpBareName(typeNode);
438
+ if (!CSHARP_RAW_MARKUP_CAST_TYPES.has(typeName))
439
+ continue;
440
+ const valueText = getNodeText(valueNode);
441
+ calls.push({
442
+ method_name: typeName,
443
+ receiver: null,
444
+ receiver_type: null,
445
+ receiver_type_fqn: null,
446
+ arguments: [{
447
+ position: 0,
448
+ expression: valueText,
449
+ variable: valueNode.type === 'identifier' ? valueText : null,
450
+ literal: CSHARP_LITERAL_NODE_TYPES.has(valueNode.type) ? valueText : null,
451
+ value: null,
452
+ }],
453
+ location: { line: cast.startPosition.row + 1, column: cast.startPosition.column },
454
+ in_method: findEnclosingMethod(cast),
455
+ is_constructor: true,
456
+ });
457
+ }
363
458
  return calls;
364
459
  }
460
+ /**
461
+ * Cast target types whose cast renders its operand as raw markup, so the cast
462
+ * is exactly as dangerous as the constructor. Deliberately tiny — see #340.
463
+ */
464
+ const CSHARP_RAW_MARKUP_CAST_TYPES = new Set(['MarkupString']);
365
465
  /** Extract args from a C# `argument_list` (each child is an `argument` wrapper). */
366
466
  function extractCSharpArguments(argsNode) {
367
467
  const args = [];
@@ -384,12 +484,38 @@ function extractCSharpArguments(argsNode) {
384
484
  position: position++,
385
485
  expression: text,
386
486
  variable: expr?.type === 'identifier' ? text : null,
387
- literal: expr?.type === 'string_literal' ? text : null,
487
+ literal: expr && CSHARP_LITERAL_NODE_TYPES.has(expr.type) ? text : null,
388
488
  value: null,
389
489
  });
390
490
  }
391
491
  return args;
392
492
  }
493
+ /**
494
+ * C# compile-time literal node types — cognium-dev#502.
495
+ *
496
+ * Only `string_literal` was recognised, so `null`, `true`, `42` and `'c'` all
497
+ * came back with `literal: null` and were indistinguishable from a variable.
498
+ * A sink taint-gated on that argument therefore registered: the measured case
499
+ * is Juliet's `new SqlCommand(null, connection)` DB-setup helper, reported as
500
+ * CWE-89 with a SQL argument that is literally `null` — 1,523 signatures over
501
+ * the 10 scored CWE families once #502 gave those files a source.
502
+ *
503
+ * A compile-time constant cannot carry taint, so registering a sink on one is
504
+ * always wrong; widening this removes false positives only.
505
+ *
506
+ * `interpolated_string_expression` is deliberately ABSENT: `$"SELECT {input}"`
507
+ * interpolates and is the genuine C# injection shape.
508
+ */
509
+ const CSHARP_LITERAL_NODE_TYPES = new Set([
510
+ 'string_literal',
511
+ 'verbatim_string_literal',
512
+ 'raw_string_literal',
513
+ 'character_literal',
514
+ 'integer_literal',
515
+ 'real_literal',
516
+ 'boolean_literal',
517
+ 'null_literal',
518
+ ]);
393
519
  function extractJavaScriptCalls(tree, cache) {
394
520
  const calls = [];
395
521
  // Build JS resolution context