circle-ir 4.8.2 → 4.9.8
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 +11 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.d.ts +1 -0
- package/dist/analysis/passes/insecure-cookie-pass.d.ts.map +1 -1
- package/dist/analysis/passes/insecure-cookie-pass.js +37 -3
- package/dist/analysis/passes/insecure-cookie-pass.js.map +1 -1
- package/dist/analysis/passes/jwt-verify-disabled-pass.d.ts.map +1 -1
- package/dist/analysis/passes/jwt-verify-disabled-pass.js +48 -21
- package/dist/analysis/passes/jwt-verify-disabled-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +131 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.d.ts.map +1 -1
- package/dist/analysis/passes/tls-verify-disabled-pass.js +34 -0
- package/dist/analysis/passes/tls-verify-disabled-pass.js.map +1 -1
- package/dist/analysis/passes/weak-crypto-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-crypto-pass.js +32 -19
- package/dist/analysis/passes/weak-crypto-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +237 -3
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +409 -43
- package/dist/core/circle-ir-core.cjs +218 -3
- package/dist/core/circle-ir-core.js +218 -3
- package/dist/core/extractors/types.js +77 -2
- package/dist/core/extractors/types.js.map +1 -1
- package/dist/wasm/web-tree-sitter.wasm +0 -0
- package/package.json +1 -1
|
@@ -4375,6 +4375,7 @@ function extractCSharpTypes(tree, cache) {
|
|
|
4375
4375
|
const nameNode = node.childForFieldName("name");
|
|
4376
4376
|
const body2 = node.childForFieldName("body");
|
|
4377
4377
|
const methods = [];
|
|
4378
|
+
const fields = extractCSharpFields(body2);
|
|
4378
4379
|
if (body2) {
|
|
4379
4380
|
for (let i2 = 0; i2 < body2.childCount; i2++) {
|
|
4380
4381
|
const m = body2.child(i2);
|
|
@@ -4392,7 +4393,7 @@ function extractCSharpTypes(tree, cache) {
|
|
|
4392
4393
|
parameters.push({
|
|
4393
4394
|
name: getNodeText(pName),
|
|
4394
4395
|
type: pType ? getNodeText(pType) : null,
|
|
4395
|
-
annotations:
|
|
4396
|
+
annotations: extractCSharpParamAnnotations(pnode),
|
|
4396
4397
|
line: pnode.startPosition.row + 1
|
|
4397
4398
|
});
|
|
4398
4399
|
}
|
|
@@ -4418,7 +4419,7 @@ function extractCSharpTypes(tree, cache) {
|
|
|
4418
4419
|
implements: [],
|
|
4419
4420
|
annotations: [],
|
|
4420
4421
|
methods,
|
|
4421
|
-
fields
|
|
4422
|
+
fields,
|
|
4422
4423
|
start_line: node.startPosition.row + 1,
|
|
4423
4424
|
end_line: node.endPosition.row + 1
|
|
4424
4425
|
});
|
|
@@ -4426,6 +4427,59 @@ function extractCSharpTypes(tree, cache) {
|
|
|
4426
4427
|
}
|
|
4427
4428
|
return types;
|
|
4428
4429
|
}
|
|
4430
|
+
function extractCSharpFields(body2) {
|
|
4431
|
+
const fields = [];
|
|
4432
|
+
if (!body2) return fields;
|
|
4433
|
+
for (let i2 = 0; i2 < body2.childCount; i2++) {
|
|
4434
|
+
const c = body2.child(i2);
|
|
4435
|
+
if (!c) continue;
|
|
4436
|
+
if (c.type === "field_declaration") {
|
|
4437
|
+
let varDecl = null;
|
|
4438
|
+
for (let k = 0; k < c.childCount; k++) {
|
|
4439
|
+
const cc = c.child(k);
|
|
4440
|
+
if (cc?.type === "variable_declaration") {
|
|
4441
|
+
varDecl = cc;
|
|
4442
|
+
break;
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
if (!varDecl) continue;
|
|
4446
|
+
const typeNode = varDecl.childForFieldName("type");
|
|
4447
|
+
const type = typeNode ? getNodeText(typeNode) : null;
|
|
4448
|
+
const modifiers = extractCSharpModifiers(c);
|
|
4449
|
+
for (let k = 0; k < varDecl.childCount; k++) {
|
|
4450
|
+
const decl = varDecl.child(k);
|
|
4451
|
+
if (decl?.type !== "variable_declarator") continue;
|
|
4452
|
+
const nameNode = decl.childForFieldName("name");
|
|
4453
|
+
fields.push({ name: nameNode ? getNodeText(nameNode) : "unknown", type, modifiers, annotations: [] });
|
|
4454
|
+
}
|
|
4455
|
+
} else if (c.type === "property_declaration") {
|
|
4456
|
+
const nameNode = c.childForFieldName("name");
|
|
4457
|
+
if (!nameNode) continue;
|
|
4458
|
+
const typeNode = c.childForFieldName("type");
|
|
4459
|
+
fields.push({
|
|
4460
|
+
name: getNodeText(nameNode),
|
|
4461
|
+
type: typeNode ? getNodeText(typeNode) : null,
|
|
4462
|
+
modifiers: extractCSharpModifiers(c),
|
|
4463
|
+
annotations: []
|
|
4464
|
+
});
|
|
4465
|
+
}
|
|
4466
|
+
}
|
|
4467
|
+
return fields;
|
|
4468
|
+
}
|
|
4469
|
+
function extractCSharpParamAnnotations(param) {
|
|
4470
|
+
const out2 = [];
|
|
4471
|
+
for (let i2 = 0; i2 < param.childCount; i2++) {
|
|
4472
|
+
const list = param.child(i2);
|
|
4473
|
+
if (list?.type !== "attribute_list") continue;
|
|
4474
|
+
for (let j = 0; j < list.childCount; j++) {
|
|
4475
|
+
const attr = list.child(j);
|
|
4476
|
+
if (attr?.type !== "attribute") continue;
|
|
4477
|
+
const name2 = attr.childForFieldName("name");
|
|
4478
|
+
if (name2) out2.push(getNodeText(name2));
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
return out2;
|
|
4482
|
+
}
|
|
4429
4483
|
function extractCSharpModifiers(node) {
|
|
4430
4484
|
const mods = [];
|
|
4431
4485
|
for (let i2 = 0; i2 < node.childCount; i2++) {
|
|
@@ -13950,6 +14004,17 @@ var DEFAULT_SINKS = [
|
|
|
13950
14004
|
{ method: "Load", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
13951
14005
|
{ method: "LoadFrom", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
13952
14006
|
{ method: "LoadFile", class: "Assembly", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
14007
|
+
// C# server-side template injection (SSTI). Compiling an attacker-controlled
|
|
14008
|
+
// template string is arbitrary code execution — classified as code_injection
|
|
14009
|
+
// (CWE-94), matching how Python (Jinja2/Mako) and Node SSTI are modelled.
|
|
14010
|
+
// Restricted to DISTINCTIVE template-compile APIs so the generic
|
|
14011
|
+
// `Template.Parse` / `.Compile` names (int.Parse, Regex.Compile, …) are not
|
|
14012
|
+
// over-matched. Taint-gated: a constant template never fires. `RunCompile`
|
|
14013
|
+
// = RazorEngine; `CompileRenderStringAsync` = RazorLight; `Compile` is
|
|
14014
|
+
// class-scoped to Handlebars. (cognium-dev#273)
|
|
14015
|
+
{ method: "RunCompile", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
14016
|
+
{ method: "CompileRenderStringAsync", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0, 1], languages: ["csharp"] },
|
|
14017
|
+
{ method: "Compile", class: "Handlebars", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0], languages: ["csharp"] },
|
|
13953
14018
|
// C# insecure deserialization — the polymorphic BCL formatters that can
|
|
13954
14019
|
// instantiate arbitrary types named in the payload (CWE-502, cognium-ai#318).
|
|
13955
14020
|
// Each of these classes exists only to deserialize and is unsafe on untrusted
|
|
@@ -15077,7 +15142,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
15077
15142
|
const skipMethods = ["toString", "hashCode", "equals", "compareTo"];
|
|
15078
15143
|
if (skipMethods.includes(method.name)) continue;
|
|
15079
15144
|
for (const param of method.parameters) {
|
|
15080
|
-
const
|
|
15145
|
+
const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
|
|
15146
|
+
const isTaintable = hasCSharpBindingAttr ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
|
|
15081
15147
|
if (isTaintable) {
|
|
15082
15148
|
const paramLine = param.line ?? method.start_line;
|
|
15083
15149
|
sources.push({
|
|
@@ -15197,6 +15263,13 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
15197
15263
|
}
|
|
15198
15264
|
return result;
|
|
15199
15265
|
}
|
|
15266
|
+
var CSHARP_BINDING_ATTRS = /* @__PURE__ */ new Set([
|
|
15267
|
+
"FromBody",
|
|
15268
|
+
"FromQuery",
|
|
15269
|
+
"FromRoute",
|
|
15270
|
+
"FromForm",
|
|
15271
|
+
"FromHeader"
|
|
15272
|
+
]);
|
|
15200
15273
|
function isInterproceduralTaintableType(typeName, language) {
|
|
15201
15274
|
const baseType = typeName.split("<")[0].trim();
|
|
15202
15275
|
const excludedTypes = [
|
|
@@ -15486,6 +15559,145 @@ function isSafeCSharpProcessStartCall(call, pattern, language, sourceLines) {
|
|
|
15486
15559
|
}
|
|
15487
15560
|
return false;
|
|
15488
15561
|
}
|
|
15562
|
+
function stripCsLiterals(line) {
|
|
15563
|
+
return line.replace(/\/\/.*$/, "").replace(/@?"(?:[^"\\]|\\.)*"/g, '""').replace(/'(?:[^'\\]|\\.)'/g, "''");
|
|
15564
|
+
}
|
|
15565
|
+
function csBraceDepthBefore(lines, idx) {
|
|
15566
|
+
let depth = 0;
|
|
15567
|
+
for (let i2 = 0; i2 < idx && i2 < lines.length; i2++) {
|
|
15568
|
+
for (const ch of stripCsLiterals(lines[i2])) {
|
|
15569
|
+
if (ch === "{") depth++;
|
|
15570
|
+
else if (ch === "}") depth--;
|
|
15571
|
+
}
|
|
15572
|
+
}
|
|
15573
|
+
return depth;
|
|
15574
|
+
}
|
|
15575
|
+
function netBraces(fragment) {
|
|
15576
|
+
let n = 0;
|
|
15577
|
+
for (const ch of fragment) {
|
|
15578
|
+
if (ch === "{") n++;
|
|
15579
|
+
else if (ch === "}") n--;
|
|
15580
|
+
}
|
|
15581
|
+
return n;
|
|
15582
|
+
}
|
|
15583
|
+
function splitCsIf(line) {
|
|
15584
|
+
const m = /\bif\s*\(/.exec(line);
|
|
15585
|
+
if (!m) return null;
|
|
15586
|
+
const ifCol = m.index;
|
|
15587
|
+
let i2 = m.index + m[0].length;
|
|
15588
|
+
let depth = 1;
|
|
15589
|
+
let inStr = false;
|
|
15590
|
+
let strCh = "";
|
|
15591
|
+
for (; i2 < line.length; i2++) {
|
|
15592
|
+
const c = line[i2];
|
|
15593
|
+
if (inStr) {
|
|
15594
|
+
if (c === "\\") {
|
|
15595
|
+
i2++;
|
|
15596
|
+
continue;
|
|
15597
|
+
}
|
|
15598
|
+
if (c === strCh) inStr = false;
|
|
15599
|
+
continue;
|
|
15600
|
+
}
|
|
15601
|
+
if (c === '"' || c === "'") {
|
|
15602
|
+
inStr = true;
|
|
15603
|
+
strCh = c;
|
|
15604
|
+
continue;
|
|
15605
|
+
}
|
|
15606
|
+
if (c === "(") depth++;
|
|
15607
|
+
else if (c === ")") {
|
|
15608
|
+
depth--;
|
|
15609
|
+
if (depth === 0) break;
|
|
15610
|
+
}
|
|
15611
|
+
}
|
|
15612
|
+
if (depth !== 0) return null;
|
|
15613
|
+
return { cond: line.slice(m.index + m[0].length, i2), rest: line.slice(i2 + 1), ifCol };
|
|
15614
|
+
}
|
|
15615
|
+
function csEqualityGuard(cond) {
|
|
15616
|
+
const c = cond.trim();
|
|
15617
|
+
let m = /^(.*?)\s*(==|!=)\s*@?"[^"]*"\s*$/.exec(c);
|
|
15618
|
+
if (m) return { op: m[2], exprSide: m[1].trim() };
|
|
15619
|
+
m = /^@?"[^"]*"\s*(==|!=)\s*(.*)$/.exec(c);
|
|
15620
|
+
if (m) return { op: m[1], exprSide: m[2].trim() };
|
|
15621
|
+
return null;
|
|
15622
|
+
}
|
|
15623
|
+
var CS_IDENT_RE = /[A-Za-z_]\w*/g;
|
|
15624
|
+
function csIdentifiers(expr) {
|
|
15625
|
+
return new Set(expr.match(CS_IDENT_RE) ?? []);
|
|
15626
|
+
}
|
|
15627
|
+
function csThenBlock(lines, ifIdx, rest) {
|
|
15628
|
+
const isExit = (s) => /^\s*(?:return|throw|continue|break)\b/.test(s);
|
|
15629
|
+
let firstIdx;
|
|
15630
|
+
let firstText;
|
|
15631
|
+
if (rest.trim()) {
|
|
15632
|
+
firstIdx = ifIdx;
|
|
15633
|
+
firstText = rest;
|
|
15634
|
+
} else {
|
|
15635
|
+
let j = ifIdx + 1;
|
|
15636
|
+
while (j < lines.length && stripCsLiterals(lines[j]).trim() === "") j++;
|
|
15637
|
+
firstIdx = j;
|
|
15638
|
+
firstText = lines[j] ?? "";
|
|
15639
|
+
}
|
|
15640
|
+
if (firstText.trim().startsWith("{")) {
|
|
15641
|
+
let depth = 0;
|
|
15642
|
+
let started = false;
|
|
15643
|
+
let endLine = lines.length - 1;
|
|
15644
|
+
let body2 = "";
|
|
15645
|
+
for (let i2 = firstIdx; i2 < lines.length; i2++) {
|
|
15646
|
+
const stripped = stripCsLiterals(i2 === firstIdx ? firstText : lines[i2]);
|
|
15647
|
+
let broke = false;
|
|
15648
|
+
for (const ch of stripped) {
|
|
15649
|
+
if (ch === "{") {
|
|
15650
|
+
depth++;
|
|
15651
|
+
started = true;
|
|
15652
|
+
if (depth === 1) continue;
|
|
15653
|
+
} else if (ch === "}") {
|
|
15654
|
+
depth--;
|
|
15655
|
+
if (depth === 0) {
|
|
15656
|
+
endLine = i2;
|
|
15657
|
+
broke = true;
|
|
15658
|
+
break;
|
|
15659
|
+
}
|
|
15660
|
+
}
|
|
15661
|
+
if (started && depth >= 1) body2 += ch;
|
|
15662
|
+
}
|
|
15663
|
+
if (broke) break;
|
|
15664
|
+
if (started) body2 += " ";
|
|
15665
|
+
}
|
|
15666
|
+
return { start: ifIdx, end: endLine, earlyExit: isExit(body2.trim()) };
|
|
15667
|
+
}
|
|
15668
|
+
return { start: ifIdx, end: firstIdx, earlyExit: isExit(firstText.trim()) };
|
|
15669
|
+
}
|
|
15670
|
+
function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines) {
|
|
15671
|
+
if (language !== "csharp") return false;
|
|
15672
|
+
if (pattern.type !== "ssrf") return false;
|
|
15673
|
+
if (!sourceLines || sourceLines.length === 0) return false;
|
|
15674
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
15675
|
+
for (const a of call.arguments) {
|
|
15676
|
+
if (a.variable) candidates.add(a.variable);
|
|
15677
|
+
for (const id of csIdentifiers(a.expression ?? "")) candidates.add(id);
|
|
15678
|
+
}
|
|
15679
|
+
if (candidates.size === 0) return false;
|
|
15680
|
+
const sinkIdx = call.location.line - 1;
|
|
15681
|
+
const sinkDepth = csBraceDepthBefore(sourceLines, sinkIdx);
|
|
15682
|
+
for (let i2 = 0; i2 < sinkIdx; i2++) {
|
|
15683
|
+
const parts2 = splitCsIf(sourceLines[i2]);
|
|
15684
|
+
if (!parts2) continue;
|
|
15685
|
+
const guard = csEqualityGuard(parts2.cond);
|
|
15686
|
+
if (!guard) continue;
|
|
15687
|
+
const guardIds = csIdentifiers(guard.exprSide);
|
|
15688
|
+
if (![...guardIds].some((id) => candidates.has(id))) continue;
|
|
15689
|
+
const block = csThenBlock(sourceLines, i2, parts2.rest);
|
|
15690
|
+
if (guard.op === "==") {
|
|
15691
|
+
if (sinkIdx >= block.start && sinkIdx <= block.end) return true;
|
|
15692
|
+
} else {
|
|
15693
|
+
const guardDepth = csBraceDepthBefore(sourceLines, i2) + netBraces(stripCsLiterals(sourceLines[i2]).slice(0, parts2.ifCol));
|
|
15694
|
+
if (block.earlyExit && sinkIdx > block.end && guardDepth === sinkDepth) {
|
|
15695
|
+
return true;
|
|
15696
|
+
}
|
|
15697
|
+
}
|
|
15698
|
+
}
|
|
15699
|
+
return false;
|
|
15700
|
+
}
|
|
15489
15701
|
function isSafeRustCommandCall(call, pattern, language) {
|
|
15490
15702
|
if (language !== "rust") return false;
|
|
15491
15703
|
if (pattern.type !== "command_injection") return false;
|
|
@@ -15744,6 +15956,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
|
|
|
15744
15956
|
if (isSafeCSharpProcessStartCall(call, pattern, language, sourceLines)) {
|
|
15745
15957
|
continue;
|
|
15746
15958
|
}
|
|
15959
|
+
if (isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)) {
|
|
15960
|
+
continue;
|
|
15961
|
+
}
|
|
15747
15962
|
if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at, types)) {
|
|
15748
15963
|
continue;
|
|
15749
15964
|
}
|
|
@@ -26630,6 +26845,8 @@ var LanguageSourcesPass = class {
|
|
|
26630
26845
|
additionalSources.push(...findSetterChainSources(types, code, language));
|
|
26631
26846
|
additionalSources.push(...findJavaScriptAssignmentSources(code, language));
|
|
26632
26847
|
additionalSources.push(...findCSharpRequestSources(code, language));
|
|
26848
|
+
additionalSources.push(...findCSharpBindingAttributeSources(code, language));
|
|
26849
|
+
additionalSources.push(...findCSharpMinimalApiSources(code, language));
|
|
26633
26850
|
additionalSources.push(...findGoArgvSources(code, language));
|
|
26634
26851
|
const jsDOMSinks = findJavaScriptDOMSinks(code, language);
|
|
26635
26852
|
for (const s of jsDOMSinks) {
|
|
@@ -27337,6 +27554,89 @@ function findCSharpRequestSources(sourceCode, language) {
|
|
|
27337
27554
|
}
|
|
27338
27555
|
return sources;
|
|
27339
27556
|
}
|
|
27557
|
+
var CSHARP_MINIMAL_API_BINDING = /* @__PURE__ */ new Map([
|
|
27558
|
+
["FromBody", "http_body"],
|
|
27559
|
+
["FromForm", "http_body"],
|
|
27560
|
+
["FromQuery", "http_param"],
|
|
27561
|
+
["FromRoute", "http_path"],
|
|
27562
|
+
["FromHeader", "http_param"]
|
|
27563
|
+
]);
|
|
27564
|
+
var CSHARP_NON_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
27565
|
+
"HttpContext",
|
|
27566
|
+
"HttpRequest",
|
|
27567
|
+
"HttpResponse",
|
|
27568
|
+
"CancellationToken",
|
|
27569
|
+
"ClaimsPrincipal",
|
|
27570
|
+
"ILogger",
|
|
27571
|
+
"IFormFileCollection"
|
|
27572
|
+
]);
|
|
27573
|
+
function findCSharpBindingAttributeSources(sourceCode, language) {
|
|
27574
|
+
if (language !== "csharp") return [];
|
|
27575
|
+
const sources = [];
|
|
27576
|
+
const lines = sourceCode.split("\n");
|
|
27577
|
+
const re = /\[\s*(FromQuery|FromBody|FromForm|FromRoute|FromHeader)(?:\s*\([^)]*\))?\s*\]\s*(?:\[[^\]]*\]\s*)*[\w.<>[\]?]+\s+([A-Za-z_]\w*)/g;
|
|
27578
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27579
|
+
const lineRe = new RegExp(re.source, "g");
|
|
27580
|
+
let m;
|
|
27581
|
+
while ((m = lineRe.exec(lines[i2])) !== null) {
|
|
27582
|
+
const attr = m[1];
|
|
27583
|
+
const name2 = m[2];
|
|
27584
|
+
const type = attr === "FromBody" || attr === "FromForm" ? "http_body" : attr === "FromRoute" ? "http_path" : "http_param";
|
|
27585
|
+
if (sources.some((s) => s.line === i2 + 1 && s.variable === name2)) continue;
|
|
27586
|
+
sources.push({
|
|
27587
|
+
type,
|
|
27588
|
+
location: `[${attr}] ${name2}`,
|
|
27589
|
+
severity: "high",
|
|
27590
|
+
line: i2 + 1,
|
|
27591
|
+
confidence: 1,
|
|
27592
|
+
variable: name2
|
|
27593
|
+
});
|
|
27594
|
+
}
|
|
27595
|
+
}
|
|
27596
|
+
return sources;
|
|
27597
|
+
}
|
|
27598
|
+
function findCSharpMinimalApiSources(sourceCode, language) {
|
|
27599
|
+
if (language !== "csharp") return [];
|
|
27600
|
+
const sources = [];
|
|
27601
|
+
const lines = sourceCode.split("\n");
|
|
27602
|
+
const mapRe = /\bMap(?:Get|Post|Put|Delete|Patch)\s*\(\s*(?:@?"[^"]*"|[\w.]+)\s*,\s*(?:\[[^\]]*\]\s*)?(?:async\s*)?\(([^)]*)\)\s*=>/;
|
|
27603
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
27604
|
+
const m = mapRe.exec(lines[i2]);
|
|
27605
|
+
if (!m || !m[1].trim()) continue;
|
|
27606
|
+
for (const rawParam of m[1].split(",")) {
|
|
27607
|
+
const seed = classifyCSharpLambdaParam(rawParam);
|
|
27608
|
+
if (!seed) continue;
|
|
27609
|
+
if (sources.some((s) => s.line === i2 + 1 && s.variable === seed.name)) continue;
|
|
27610
|
+
sources.push({
|
|
27611
|
+
type: seed.type,
|
|
27612
|
+
location: `${seed.name} (Minimal API ${seed.via})`,
|
|
27613
|
+
severity: "high",
|
|
27614
|
+
line: i2 + 1,
|
|
27615
|
+
confidence: 1,
|
|
27616
|
+
variable: seed.name
|
|
27617
|
+
});
|
|
27618
|
+
}
|
|
27619
|
+
}
|
|
27620
|
+
return sources;
|
|
27621
|
+
}
|
|
27622
|
+
function classifyCSharpLambdaParam(raw) {
|
|
27623
|
+
const attrs = [...raw.matchAll(/\[([^\]]*)\]/g)].map((a) => a[1].split("(")[0].trim());
|
|
27624
|
+
if (attrs.includes("FromServices")) return null;
|
|
27625
|
+
if (attrs.some((a) => CSHARP_MINIMAL_API_BINDING.has(a))) return null;
|
|
27626
|
+
const noAttr = raw.replace(/\[[^\]]*\]/g, "").trim();
|
|
27627
|
+
const parts2 = noAttr.split(/\s+/).filter(Boolean);
|
|
27628
|
+
if (parts2.length < 2) return null;
|
|
27629
|
+
const name2 = parts2[parts2.length - 1];
|
|
27630
|
+
if (!/^[A-Za-z_]\w*$/.test(name2)) return null;
|
|
27631
|
+
const baseType = (parts2[parts2.length - 2] ?? "").replace(/[?\[\]]/g, "").split("<")[0].split(".").pop() ?? "";
|
|
27632
|
+
if (CSHARP_NON_INPUT_TYPES.has(baseType)) return null;
|
|
27633
|
+
for (const a of attrs) {
|
|
27634
|
+
const t = CSHARP_MINIMAL_API_BINDING.get(a);
|
|
27635
|
+
if (t) return { type: t, name: name2, via: `[${a}]` };
|
|
27636
|
+
}
|
|
27637
|
+
if (baseType === "string") return { type: "http_param", name: name2, via: "string param" };
|
|
27638
|
+
return null;
|
|
27639
|
+
}
|
|
27340
27640
|
function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
27341
27641
|
if (!["javascript", "typescript"].includes(language)) return [];
|
|
27342
27642
|
const sources = [];
|
|
@@ -41662,6 +41962,9 @@ var JAVA_SET_HTTPONLY_TRUE_RE = /\.setHttpOnly\s*\(\s*true\s*\)/;
|
|
|
41662
41962
|
var GO_SECURE_TRUE_RE = /\bSecure\s*:\s*true\b/;
|
|
41663
41963
|
var GO_HTTPONLY_TRUE_RE = /\bHttpOnly\s*:\s*true\b/;
|
|
41664
41964
|
var RUST_SET_COOKIE_MACRO_RE = /(format!|write!|writeln!)\s*\(([^()]*Set-Cookie[^()]*)\)/gis;
|
|
41965
|
+
var CS_COOKIE_OPTIONS_RE = /\bnew\s+CookieOptions\s*\{([^{}]*)\}/gs;
|
|
41966
|
+
var CS_SECURE_FALSE_RE = /\bSecure\s*=\s*false\b/;
|
|
41967
|
+
var CS_HTTPONLY_FALSE_RE = /\bHttpOnly\s*=\s*false\b/;
|
|
41665
41968
|
var InsecureCookiePass = class {
|
|
41666
41969
|
name = "insecure-cookie";
|
|
41667
41970
|
category = "security";
|
|
@@ -41705,9 +42008,29 @@ var InsecureCookiePass = class {
|
|
|
41705
42008
|
insecureCookies.push(det);
|
|
41706
42009
|
this.emit(ctx, file, det, "rust");
|
|
41707
42010
|
}
|
|
42011
|
+
} else if (language === "csharp") {
|
|
42012
|
+
for (const det of this.detectCSharpCookieOptions(code)) {
|
|
42013
|
+
insecureCookies.push(det);
|
|
42014
|
+
this.emit(ctx, file, det, "csharp");
|
|
42015
|
+
}
|
|
41708
42016
|
}
|
|
41709
42017
|
return { insecureCookies };
|
|
41710
42018
|
}
|
|
42019
|
+
// ---------------- C# ----------------
|
|
42020
|
+
detectCSharpCookieOptions(code) {
|
|
42021
|
+
const out2 = [];
|
|
42022
|
+
const re = new RegExp(CS_COOKIE_OPTIONS_RE.source, CS_COOKIE_OPTIONS_RE.flags);
|
|
42023
|
+
let m;
|
|
42024
|
+
while ((m = re.exec(code)) !== null) {
|
|
42025
|
+
const body2 = m[1] ?? "";
|
|
42026
|
+
const missingSecure = CS_SECURE_FALSE_RE.test(body2);
|
|
42027
|
+
const missingHttpOnly = CS_HTTPONLY_FALSE_RE.test(body2);
|
|
42028
|
+
if (!missingSecure && !missingHttpOnly) continue;
|
|
42029
|
+
const line = code.slice(0, m.index).split("\n").length;
|
|
42030
|
+
out2.push({ line, receiver: "CookieOptions", missingSecure, missingHttpOnly, optionsPresent: true });
|
|
42031
|
+
}
|
|
42032
|
+
return out2;
|
|
42033
|
+
}
|
|
41711
42034
|
// ---------------- JS / TS ----------------
|
|
41712
42035
|
detectJs(call) {
|
|
41713
42036
|
if (call.method_name !== "cookie") return null;
|
|
@@ -41809,15 +42132,15 @@ var InsecureCookiePass = class {
|
|
|
41809
42132
|
const missing = [];
|
|
41810
42133
|
if (det.missingSecure) {
|
|
41811
42134
|
missing.push(
|
|
41812
|
-
flavor === "js" ? "`secure: true`" : flavor === "python" ? "`secure=True`" : flavor === "java" ? "`setSecure(true)`" : flavor === "go" ? "`Secure: true`" : "`Secure` attribute"
|
|
42135
|
+
flavor === "js" ? "`secure: true`" : flavor === "python" ? "`secure=True`" : flavor === "java" ? "`setSecure(true)`" : flavor === "go" ? "`Secure: true`" : flavor === "csharp" ? "`Secure = true`" : "`Secure` attribute"
|
|
41813
42136
|
);
|
|
41814
42137
|
}
|
|
41815
42138
|
if (det.missingHttpOnly) {
|
|
41816
42139
|
missing.push(
|
|
41817
|
-
flavor === "js" ? "`httpOnly: true`" : flavor === "python" ? "`httponly=True`" : flavor === "java" ? "`setHttpOnly(true)`" : flavor === "go" ? "`HttpOnly: true`" : "`HttpOnly` attribute"
|
|
42140
|
+
flavor === "js" ? "`httpOnly: true`" : flavor === "python" ? "`httponly=True`" : flavor === "java" ? "`setHttpOnly(true)`" : flavor === "go" ? "`HttpOnly: true`" : flavor === "csharp" ? "`HttpOnly = true`" : "`HttpOnly` attribute"
|
|
41818
42141
|
);
|
|
41819
42142
|
}
|
|
41820
|
-
const fix = flavor === "js" ? 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.' : flavor === "python" ? 'Pass `secure=True, httponly=True, samesite="Lax"` to `response.set_cookie(...)`.' : flavor === "java" ? "After constructing the cookie, call `cookie.setSecure(true)` and `cookie.setHttpOnly(true)` before adding it to the response." : flavor === "go" ? "Set `Secure: true` and `HttpOnly: true` on the `http.Cookie` struct literal passed to `http.SetCookie`." : "Append `; Secure; HttpOnly` to the `Set-Cookie` header string.";
|
|
42143
|
+
const fix = flavor === "js" ? 'Pass `{ secure: true, httpOnly: true, sameSite: "lax" }` as the third argument to `res.cookie()`.' : flavor === "python" ? 'Pass `secure=True, httponly=True, samesite="Lax"` to `response.set_cookie(...)`.' : flavor === "java" ? "After constructing the cookie, call `cookie.setSecure(true)` and `cookie.setHttpOnly(true)` before adding it to the response." : flavor === "go" ? "Set `Secure: true` and `HttpOnly: true` on the `http.Cookie` struct literal passed to `http.SetCookie`." : flavor === "csharp" ? "Set `Secure = true` and `HttpOnly = true` on the `CookieOptions` (or enforce them globally via `CookiePolicyOptions`)." : "Append `; Secure; HttpOnly` to the `Set-Cookie` header string.";
|
|
41821
42144
|
ctx.addFinding({
|
|
41822
42145
|
id: `${this.name}-${file}-${det.line}`,
|
|
41823
42146
|
pass: this.name,
|
|
@@ -42288,6 +42611,7 @@ var ISSUE_CWE = {
|
|
|
42288
42611
|
"hardcoded-key": "CWE-321",
|
|
42289
42612
|
"weak-rsa-key": "CWE-326"
|
|
42290
42613
|
};
|
|
42614
|
+
var CS_CIPHER_MODE_ECB_RE = /\bCipherMode\s*\.\s*ECB\b/;
|
|
42291
42615
|
var WeakCryptoPass = class {
|
|
42292
42616
|
name = "weak-crypto";
|
|
42293
42617
|
category = "security";
|
|
@@ -42303,26 +42627,34 @@ var WeakCryptoPass = class {
|
|
|
42303
42627
|
const findings = [];
|
|
42304
42628
|
const constProp = ctx.hasResult("constant-propagation") ? ctx.getResult("constant-propagation") : null;
|
|
42305
42629
|
const literalBindings = scanLiteralBindings(code, language);
|
|
42630
|
+
const emit = (line, det) => {
|
|
42631
|
+
findings.push({ line, language, ...det });
|
|
42632
|
+
ctx.addFinding({
|
|
42633
|
+
id: `${this.name}-${file}-${line}-${det.issue}`,
|
|
42634
|
+
pass: this.name,
|
|
42635
|
+
category: this.category,
|
|
42636
|
+
rule_id: this.name,
|
|
42637
|
+
cwe: ISSUE_CWE[det.issue],
|
|
42638
|
+
severity: "high",
|
|
42639
|
+
level: "error",
|
|
42640
|
+
message: this.buildMessage(det),
|
|
42641
|
+
file,
|
|
42642
|
+
line,
|
|
42643
|
+
fix: this.buildFix(det.issue),
|
|
42644
|
+
evidence: { ...det, language }
|
|
42645
|
+
});
|
|
42646
|
+
};
|
|
42306
42647
|
for (const call of graph.ir.calls) {
|
|
42307
|
-
const
|
|
42308
|
-
|
|
42309
|
-
|
|
42310
|
-
|
|
42311
|
-
|
|
42312
|
-
|
|
42313
|
-
|
|
42314
|
-
|
|
42315
|
-
|
|
42316
|
-
|
|
42317
|
-
cwe: ISSUE_CWE[det.issue],
|
|
42318
|
-
severity: "high",
|
|
42319
|
-
level: "error",
|
|
42320
|
-
message,
|
|
42321
|
-
file,
|
|
42322
|
-
line,
|
|
42323
|
-
fix: this.buildFix(det.issue),
|
|
42324
|
-
evidence: { ...det, language }
|
|
42325
|
-
});
|
|
42648
|
+
for (const det of this.detect(call, language, constProp, literalBindings)) {
|
|
42649
|
+
emit(call.location.line, det);
|
|
42650
|
+
}
|
|
42651
|
+
}
|
|
42652
|
+
if (language === "csharp") {
|
|
42653
|
+
const lines = code.split("\n");
|
|
42654
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
42655
|
+
if (CS_CIPHER_MODE_ECB_RE.test(lines[i2])) {
|
|
42656
|
+
emit(i2 + 1, { issue: "ecb-mode", detail: "CipherMode.ECB", api: "SymmetricAlgorithm.Mode" });
|
|
42657
|
+
}
|
|
42326
42658
|
}
|
|
42327
42659
|
}
|
|
42328
42660
|
return { findings };
|
|
@@ -43874,6 +44206,8 @@ var VERIFY_FALSE_RE = /\bverify\s*=\s*False\b/;
|
|
|
43874
44206
|
var REJECT_UNAUTHORIZED_FALSE_RE = /\brejectUnauthorized\s*:\s*false\b/;
|
|
43875
44207
|
var INSECURE_SKIP_VERIFY_TRUE_RE = /\bInsecureSkipVerify\s*:\s*true\b/;
|
|
43876
44208
|
var HOSTNAME_LAMBDA_TRUE_RE = /\(\s*\w+\s*,\s*\w+\s*\)\s*->\s*true\b/;
|
|
44209
|
+
var CS_CERT_CALLBACK_TRUE_RE = /\b(ServerCertificateValidationCallback|ServerCertificateCustomValidationCallback|RemoteCertificateValidationCallback)\s*(?:\+?=|\()\s*(?:\([^)]*\)|\w+)\s*=>\s*(?:true\b|\{\s*return\s+true\b)/;
|
|
44210
|
+
var CS_DANGEROUS_ACCEPT_RE = /\bDangerousAcceptAnyServerCertificateValidator\b/;
|
|
43877
44211
|
var ALLOW_ALL_HOSTNAME_VERIFIERS = /* @__PURE__ */ new Set([
|
|
43878
44212
|
"NoopHostnameVerifier.INSTANCE",
|
|
43879
44213
|
"new AllowAllHostnameVerifier()",
|
|
@@ -44018,6 +44352,21 @@ var TlsVerifyDisabledPass = class {
|
|
|
44018
44352
|
}
|
|
44019
44353
|
}
|
|
44020
44354
|
}
|
|
44355
|
+
if (language === "csharp") {
|
|
44356
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
44357
|
+
const l = lines[i2];
|
|
44358
|
+
const m = CS_CERT_CALLBACK_TRUE_RE.exec(l);
|
|
44359
|
+
if (m) {
|
|
44360
|
+
out2.push({ line: i2 + 1, pattern: `${m[1]} => true`, api: m[1] });
|
|
44361
|
+
} else if (CS_DANGEROUS_ACCEPT_RE.test(l)) {
|
|
44362
|
+
out2.push({
|
|
44363
|
+
line: i2 + 1,
|
|
44364
|
+
pattern: "DangerousAcceptAnyServerCertificateValidator",
|
|
44365
|
+
api: "HttpClientHandler"
|
|
44366
|
+
});
|
|
44367
|
+
}
|
|
44368
|
+
}
|
|
44369
|
+
}
|
|
44021
44370
|
return out2;
|
|
44022
44371
|
}
|
|
44023
44372
|
fixFor(language, pattern) {
|
|
@@ -44039,6 +44388,9 @@ var TlsVerifyDisabledPass = class {
|
|
|
44039
44388
|
if (pattern.includes("ssl._create_unverified_context")) {
|
|
44040
44389
|
return "Do not use `_create_unverified_context()`. Use `ssl.create_default_context()`.";
|
|
44041
44390
|
}
|
|
44391
|
+
if (language === "csharp") {
|
|
44392
|
+
return "Do not accept every certificate. Remove the always-true validation callback (and `DangerousAcceptAnyServerCertificateValidator`); rely on the platform default. To trust a private CA, validate the chain against it in the callback instead of returning true.";
|
|
44393
|
+
}
|
|
44042
44394
|
return "Restore TLS certificate and hostname verification.";
|
|
44043
44395
|
}
|
|
44044
44396
|
};
|
|
@@ -44542,6 +44894,8 @@ var PY_VERIFY_SIGNATURE_FALSE_RE = /["']verify_signature["']\s*:\s*False\b/;
|
|
|
44542
44894
|
var PY_VERIFY_KW_FALSE_RE = /\bverify\s*=\s*False\b/;
|
|
44543
44895
|
var PY_ALG_NONE_RE = /\balgorithms\s*=\s*[\[\(]\s*["']none["']/i;
|
|
44544
44896
|
var JS_ALG_NONE_RE = /\balgorithms\s*:\s*\[\s*["']none["']/i;
|
|
44897
|
+
var CS_REQUIRE_SIGNED_FALSE_RE = /\bRequireSignedTokens\s*=\s*false\b/;
|
|
44898
|
+
var CS_SIGNATURE_VALIDATOR_BYPASS_RE = /\bSignatureValidator\s*=\s*[^;]*=>\s*new\s+JwtSecurityToken\b/;
|
|
44545
44899
|
var JwtVerifyDisabledPass = class {
|
|
44546
44900
|
name = "jwt-verify-disabled";
|
|
44547
44901
|
category = "security";
|
|
@@ -44549,25 +44903,34 @@ var JwtVerifyDisabledPass = class {
|
|
|
44549
44903
|
const { graph, language } = ctx;
|
|
44550
44904
|
const file = graph.ir.meta.file;
|
|
44551
44905
|
const findings = [];
|
|
44906
|
+
const emit = (line, det) => {
|
|
44907
|
+
findings.push({ line, language, ...det });
|
|
44908
|
+
ctx.addFinding({
|
|
44909
|
+
id: `${this.name}-${file}-${line}-${det.pattern}`,
|
|
44910
|
+
pass: this.name,
|
|
44911
|
+
category: this.category,
|
|
44912
|
+
rule_id: this.name,
|
|
44913
|
+
cwe: "CWE-347",
|
|
44914
|
+
severity: "critical",
|
|
44915
|
+
level: "error",
|
|
44916
|
+
message: `JWT signature verification disabled via \`${det.pattern}\` in \`${det.api}\`. Any attacker can forge a token with arbitrary claims (user id, roles, expiry) since the signature is not checked.`,
|
|
44917
|
+
file,
|
|
44918
|
+
line,
|
|
44919
|
+
fix: this.fixFor(language),
|
|
44920
|
+
evidence: { ...det, language }
|
|
44921
|
+
});
|
|
44922
|
+
};
|
|
44552
44923
|
for (const call of graph.ir.calls) {
|
|
44553
|
-
const
|
|
44554
|
-
|
|
44555
|
-
|
|
44556
|
-
|
|
44557
|
-
|
|
44558
|
-
|
|
44559
|
-
|
|
44560
|
-
|
|
44561
|
-
|
|
44562
|
-
|
|
44563
|
-
severity: "critical",
|
|
44564
|
-
level: "error",
|
|
44565
|
-
message: `JWT signature verification disabled via \`${det.pattern}\` in \`${det.api}\`. Any attacker can forge a token with arbitrary claims (user id, roles, expiry) since the signature is not checked.`,
|
|
44566
|
-
file,
|
|
44567
|
-
line,
|
|
44568
|
-
fix: this.fixFor(language),
|
|
44569
|
-
evidence: { ...det, language }
|
|
44570
|
-
});
|
|
44924
|
+
for (const det of this.detect(call, language)) emit(call.location.line, det);
|
|
44925
|
+
}
|
|
44926
|
+
if (language === "csharp") {
|
|
44927
|
+
const lines = ctx.code.split("\n");
|
|
44928
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
44929
|
+
if (CS_REQUIRE_SIGNED_FALSE_RE.test(lines[i2])) {
|
|
44930
|
+
emit(i2 + 1, { pattern: "RequireSignedTokens = false", api: "TokenValidationParameters" });
|
|
44931
|
+
} else if (CS_SIGNATURE_VALIDATOR_BYPASS_RE.test(lines[i2])) {
|
|
44932
|
+
emit(i2 + 1, { pattern: "SignatureValidator returns an unvalidated token", api: "TokenValidationParameters" });
|
|
44933
|
+
}
|
|
44571
44934
|
}
|
|
44572
44935
|
}
|
|
44573
44936
|
return { findings };
|
|
@@ -44639,6 +45002,9 @@ var JwtVerifyDisabledPass = class {
|
|
|
44639
45002
|
if (language === "java") {
|
|
44640
45003
|
return "For auth0/java-jwt: use `JWT.require(Algorithm.HMAC256(secret))` or an RSA algorithm. For jjwt: call `parseClaimsJws(token)` (signature enforced) rather than `parse(token)` (signature ignored).";
|
|
44641
45004
|
}
|
|
45005
|
+
if (language === "csharp") {
|
|
45006
|
+
return "Leave `RequireSignedTokens = true` and do not install a custom `SignatureValidator` that returns the token unverified. Configure `IssuerSigningKey`/`IssuerSigningKeys` and let the handler validate the signature.";
|
|
45007
|
+
}
|
|
44642
45008
|
return "Enforce JWT signature verification with a concrete algorithm (HS256/RS256/ES256). Never accept `alg: none`.";
|
|
44643
45009
|
}
|
|
44644
45010
|
};
|