circle-ir 4.9.26 → 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 +23 -3
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +154 -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 +156 -4
- 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/analyzer.d.ts +36 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +30 -0
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +360 -13
- package/dist/core/circle-ir-core.cjs +94 -5
- package/dist/core/circle-ir-core.js +94 -5
- package/dist/core/extractors/calls.js +127 -1
- package/dist/core/extractors/calls.js.map +1 -1
- package/dist/types/index.d.ts +17 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
|
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
|
-
//
|
|
13315
|
-
|
|
13316
|
-
|
|
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] },
|
|
@@ -13710,10 +13783,19 @@ var DEFAULT_SINKS = [
|
|
|
13710
13783
|
// C# XSS — raw HTML output (CWE-79).
|
|
13711
13784
|
{ method: "Raw", class: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13712
13785
|
{ method: "Write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13786
|
+
// ASP.NET Core writes the response body through the `HttpResponseWritingExtensions`
|
|
13787
|
+
// extension methods rather than `Response.Write`, so the System.Web row above does
|
|
13788
|
+
// not cover it. Receiver-scoped to `Response` so the many unrelated `WriteAsync`
|
|
13789
|
+
// overloads (Stream, StreamWriter, TextWriter, PipeWriter) are untouched. (#503)
|
|
13790
|
+
{ method: "WriteAsync", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13713
13791
|
{ method: "HtmlString", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13714
13792
|
// Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
|
|
13715
13793
|
// documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
|
|
13716
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"] },
|
|
13717
13799
|
// `return Content(html, "text/html")` on a controller writes arg 0 to the
|
|
13718
13800
|
// response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
|
|
13719
13801
|
// `Content(constantString)` and the JSON/plain-text content types are
|
|
@@ -13728,6 +13810,12 @@ var DEFAULT_SINKS = [
|
|
|
13728
13810
|
// filter is the constructor argument.
|
|
13729
13811
|
{ method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13730
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"] },
|
|
13731
13819
|
// C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
|
|
13732
13820
|
// cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
|
|
13733
13821
|
// an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
|
|
@@ -16072,8 +16160,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
16072
16160
|
return false;
|
|
16073
16161
|
} else if (!call.receiver && !call.receiver_type) {
|
|
16074
16162
|
const target = call.resolution?.target;
|
|
16163
|
+
const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
|
|
16075
16164
|
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
16076
|
-
if (
|
|
16165
|
+
if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
|
|
16077
16166
|
} else {
|
|
16078
16167
|
return false;
|
|
16079
16168
|
}
|
|
@@ -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] },
|
|
@@ -13644,10 +13717,19 @@ var DEFAULT_SINKS = [
|
|
|
13644
13717
|
// C# XSS — raw HTML output (CWE-79).
|
|
13645
13718
|
{ method: "Raw", class: "Html", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13646
13719
|
{ method: "Write", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13720
|
+
// ASP.NET Core writes the response body through the `HttpResponseWritingExtensions`
|
|
13721
|
+
// extension methods rather than `Response.Write`, so the System.Web row above does
|
|
13722
|
+
// not cover it. Receiver-scoped to `Response` so the many unrelated `WriteAsync`
|
|
13723
|
+
// overloads (Stream, StreamWriter, TextWriter, PipeWriter) are untouched. (#503)
|
|
13724
|
+
{ method: "WriteAsync", class: "Response", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13647
13725
|
{ method: "HtmlString", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13648
13726
|
// Blazor `new MarkupString(x)` renders its argument as raw HTML (the framework's
|
|
13649
13727
|
// documented "trusted markup" escape hatch) — attacker-controlled input is XSS. (ca#275)
|
|
13650
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"] },
|
|
13651
13733
|
// `return Content(html, "text/html")` on a controller writes arg 0 to the
|
|
13652
13734
|
// response body unescaped (cognium-dev#275). Taint-gated on arg 0; the common
|
|
13653
13735
|
// `Content(constantString)` and the JSON/plain-text content types are
|
|
@@ -13662,6 +13744,12 @@ var DEFAULT_SINKS = [
|
|
|
13662
13744
|
// filter is the constructor argument.
|
|
13663
13745
|
{ method: "DirectorySearcher", type: "ldap_injection", cwe: "CWE-90", severity: "high", arg_positions: [0], languages: ["csharp"] },
|
|
13664
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"] },
|
|
13665
13753
|
// C# open redirect — ASP.NET MVC / Minimal API redirect helpers (CWE-601,
|
|
13666
13754
|
// cognium-dev#273/#275). Classless: `return Redirect(url)` on a controller is
|
|
13667
13755
|
// an implicit-`this` call (no receiver), and `Results.Redirect(url)` /
|
|
@@ -16006,8 +16094,9 @@ function matchesSinkPattern(call, pattern, typeHierarchy, language) {
|
|
|
16006
16094
|
return false;
|
|
16007
16095
|
} else if (!call.receiver && !call.receiver_type) {
|
|
16008
16096
|
const target = call.resolution?.target;
|
|
16097
|
+
const normalisedTarget = language === "rust" ? target?.replace(/::/g, ".") : target;
|
|
16009
16098
|
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
16010
|
-
if (
|
|
16099
|
+
if (normalisedTarget && (normalisedTarget === expectedTail || normalisedTarget.endsWith("." + expectedTail))) {
|
|
16011
16100
|
} else {
|
|
16012
16101
|
return false;
|
|
16013
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
|