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.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +18 -3
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +130 -0
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +63 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +108 -1
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +97 -4
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/variable-shadowing-pass.d.ts.map +1 -1
- package/dist/analysis/passes/variable-shadowing-pass.js +15 -0
- package/dist/analysis/passes/variable-shadowing-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +21 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +322 -10
- package/dist/core/circle-ir-core.cjs +89 -5
- package/dist/core/circle-ir-core.js +89 -5
- package/dist/core/extractors/calls.js +127 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
//
|
|
13249
|
-
|
|
13250
|
-
|
|
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 (
|
|
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
|
|
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
|