circle-ir 4.12.0 → 4.13.0
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/README.md +9 -9
- package/dist/analysis/constant-propagation/propagator.d.ts +2 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +41 -2
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +4 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +147 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.js +564 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -0
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +89 -0
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +49 -52
- package/dist/analysis/passes/sink-filter-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 +328 -51
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +801 -83
- package/dist/core/circle-ir-core.cjs +41 -2
- package/dist/core/circle-ir-core.js +41 -2
- package/dist/core/extractors/dfg.js +17 -0
- package/dist/core/extractors/dfg.js.map +1 -1
- package/package.json +1 -1
|
@@ -9621,6 +9621,7 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9621
9621
|
currentScope(scopeStack).set(name2, def.id);
|
|
9622
9622
|
}
|
|
9623
9623
|
}
|
|
9624
|
+
const plainTargets = [];
|
|
9624
9625
|
for (const asn of findNodes(body2, "assignment_expression")) {
|
|
9625
9626
|
const left = asn.childForFieldName("left");
|
|
9626
9627
|
if (left?.type === "identifier") {
|
|
@@ -9628,9 +9629,16 @@ function buildCSharpDFG(tree, cache) {
|
|
|
9628
9629
|
const def = { id: defIdCounter++, variable: name2, line: asn.startPosition.row + 1, kind: "local" };
|
|
9629
9630
|
defs.push(def);
|
|
9630
9631
|
currentScope(scopeStack).set(name2, def.id);
|
|
9632
|
+
if (asn.children.some((c) => c?.type === "=")) {
|
|
9633
|
+
plainTargets.push({ name: name2, line: left.startPosition.row + 1 });
|
|
9634
|
+
}
|
|
9631
9635
|
}
|
|
9632
9636
|
}
|
|
9633
9637
|
const bodyUses = extractUses(body2, useIdCounter, scopeStack, false);
|
|
9638
|
+
for (const target of plainTargets) {
|
|
9639
|
+
const idx = bodyUses.uses.findIndex((u) => u.variable === target.name && u.line === target.line);
|
|
9640
|
+
if (idx !== -1) bodyUses.uses.splice(idx, 1);
|
|
9641
|
+
}
|
|
9634
9642
|
resolveReassignedUses(defs.slice(methodDefStart), bodyUses.uses);
|
|
9635
9643
|
uses.push(...bodyUses.uses);
|
|
9636
9644
|
useIdCounter = bodyUses.nextId;
|
|
@@ -19733,7 +19741,16 @@ var CSHARP_NON_ESCAPE_METHODS = /* @__PURE__ */ new Set([
|
|
|
19733
19741
|
"TryAdd",
|
|
19734
19742
|
"Insert",
|
|
19735
19743
|
"Push",
|
|
19736
|
-
"Enqueue"
|
|
19744
|
+
"Enqueue",
|
|
19745
|
+
// Encoders (cognium-dev#518). Encoding a value hands back a string; nothing
|
|
19746
|
+
// has left the program at `Uri.EscapeDataString(input)`.
|
|
19747
|
+
"EscapeDataString",
|
|
19748
|
+
"EscapeUriString",
|
|
19749
|
+
"UrlEncode",
|
|
19750
|
+
"UrlPathEncode",
|
|
19751
|
+
"HtmlEncode",
|
|
19752
|
+
"HtmlAttributeEncode",
|
|
19753
|
+
"JavaScriptStringEncode"
|
|
19737
19754
|
]);
|
|
19738
19755
|
function isCSharpNonEscapeCall(call) {
|
|
19739
19756
|
const name2 = call.method_name;
|
|
@@ -22151,7 +22168,16 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22151
22168
|
break;
|
|
22152
22169
|
}
|
|
22153
22170
|
}
|
|
22154
|
-
|
|
22171
|
+
if (!switchValue) {
|
|
22172
|
+
const valueNode = node.childForFieldName("value");
|
|
22173
|
+
if (valueNode) {
|
|
22174
|
+
const literal = this.literalValue(valueNode);
|
|
22175
|
+
switchValue = literal !== null ? { type: typeof literal === "number" ? "int" : "string", value: literal, sourceLine: getNodeLine(valueNode) } : this.evaluateExpression(valueNode);
|
|
22176
|
+
}
|
|
22177
|
+
}
|
|
22178
|
+
const switchBlock = node.children.find(
|
|
22179
|
+
(c) => c.type === "switch_block" || c.type === "switch_body"
|
|
22180
|
+
);
|
|
22155
22181
|
if (!switchBlock) {
|
|
22156
22182
|
for (const child of node.children) {
|
|
22157
22183
|
this.visit(child);
|
|
@@ -22159,7 +22185,7 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22159
22185
|
return;
|
|
22160
22186
|
}
|
|
22161
22187
|
const caseGroups = switchBlock.children.filter(
|
|
22162
|
-
(c) => c.type === "switch_block_statement_group" || c.type === "switch_rule"
|
|
22188
|
+
(c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section"
|
|
22163
22189
|
);
|
|
22164
22190
|
if (switchValue && isKnown(switchValue)) {
|
|
22165
22191
|
let matchingIdx = -1;
|
|
@@ -22177,6 +22203,16 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22177
22203
|
matchingIdx = i2;
|
|
22178
22204
|
}
|
|
22179
22205
|
}
|
|
22206
|
+
} else if (caseGroup.type === "switch_section") {
|
|
22207
|
+
if (child.type === "default") {
|
|
22208
|
+
defaultIdx = i2;
|
|
22209
|
+
} else if (child.type === "constant_pattern") {
|
|
22210
|
+
const inner = child.children.find((c) => c.isNamed);
|
|
22211
|
+
const caseValue = inner ? this.literalValue(inner) : null;
|
|
22212
|
+
if (caseValue !== null && caseValue === switchValue.value) {
|
|
22213
|
+
matchingIdx = i2;
|
|
22214
|
+
}
|
|
22215
|
+
}
|
|
22180
22216
|
}
|
|
22181
22217
|
}
|
|
22182
22218
|
}
|
|
@@ -22258,6 +22294,18 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22258
22294
|
}
|
|
22259
22295
|
return false;
|
|
22260
22296
|
}
|
|
22297
|
+
/** The value of an integer, character or string literal node; null for anything else. */
|
|
22298
|
+
literalValue(node) {
|
|
22299
|
+
const text = getNodeText2(node, this.source);
|
|
22300
|
+
if (node.type === "integer_literal" || node.type === "decimal_integer_literal") {
|
|
22301
|
+
const n = Number(text.replace(/_/g, "").replace(/[uUlL]+$/, ""));
|
|
22302
|
+
return Number.isInteger(n) ? n : null;
|
|
22303
|
+
}
|
|
22304
|
+
if (node.type === "character_literal" || node.type === "string_literal") {
|
|
22305
|
+
return text.slice(1, -1);
|
|
22306
|
+
}
|
|
22307
|
+
return null;
|
|
22308
|
+
}
|
|
22261
22309
|
extractCaseValue(labelNode) {
|
|
22262
22310
|
for (const child of labelNode.children) {
|
|
22263
22311
|
if (child.type === "decimal_integer_literal") {
|
|
@@ -31317,7 +31365,7 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
31317
31365
|
if (!m) continue;
|
|
31318
31366
|
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
31319
31367
|
const guarded = /* @__PURE__ */ new Set([m[1]]);
|
|
31320
|
-
const
|
|
31368
|
+
const mentions2 = (t) => {
|
|
31321
31369
|
for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
|
|
31322
31370
|
return false;
|
|
31323
31371
|
};
|
|
@@ -31325,10 +31373,10 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
31325
31373
|
const lt = lines[l];
|
|
31326
31374
|
const a = assignRe.exec(lt);
|
|
31327
31375
|
if (a) {
|
|
31328
|
-
if (
|
|
31376
|
+
if (mentions2(a[2])) guarded.add(a[1]);
|
|
31329
31377
|
else if (a[1] !== m[1]) guarded.delete(a[1]);
|
|
31330
31378
|
}
|
|
31331
|
-
if (
|
|
31379
|
+
if (mentions2(lt)) {
|
|
31332
31380
|
sanitizers.push({
|
|
31333
31381
|
type: "java_canonical_startswith_guard",
|
|
31334
31382
|
method: "getCanonicalPath",
|
|
@@ -31384,14 +31432,14 @@ function findCSharpEarlyReturnGuardSanitizers(code, language) {
|
|
|
31384
31432
|
if (!a || !guarded.has(a[1])) continue;
|
|
31385
31433
|
for (const id of a[2].match(/[A-Za-z_]\w*/g) ?? []) guarded.add(id);
|
|
31386
31434
|
}
|
|
31387
|
-
const
|
|
31435
|
+
const mentions2 = (t) => {
|
|
31388
31436
|
for (const n of guarded) if (new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(t)) return true;
|
|
31389
31437
|
return false;
|
|
31390
31438
|
};
|
|
31391
31439
|
for (let l = i2 + 1; l < lines.length; l++) {
|
|
31392
31440
|
const a = assignRe.exec(lines[l]);
|
|
31393
|
-
if (a &&
|
|
31394
|
-
if (
|
|
31441
|
+
if (a && mentions2(a[2])) guarded.add(a[1]);
|
|
31442
|
+
if (mentions2(lines[l])) {
|
|
31395
31443
|
sanitizers.push({
|
|
31396
31444
|
type: "csharp_early_return_guard",
|
|
31397
31445
|
method: "guard",
|
|
@@ -31429,7 +31477,7 @@ function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
|
31429
31477
|
if (!m || !canonicalVars.has(m[1])) continue;
|
|
31430
31478
|
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
31431
31479
|
const guarded = /* @__PURE__ */ new Set([m[1]]);
|
|
31432
|
-
const
|
|
31480
|
+
const mentions2 = (t) => {
|
|
31433
31481
|
for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
|
|
31434
31482
|
return false;
|
|
31435
31483
|
};
|
|
@@ -31437,10 +31485,10 @@ function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
|
31437
31485
|
const lt = lines[l];
|
|
31438
31486
|
const a = assignRe.exec(lt);
|
|
31439
31487
|
if (a) {
|
|
31440
|
-
if (
|
|
31488
|
+
if (mentions2(a[2])) guarded.add(a[1]);
|
|
31441
31489
|
else if (a[1] !== m[1]) guarded.delete(a[1]);
|
|
31442
31490
|
}
|
|
31443
|
-
if (
|
|
31491
|
+
if (mentions2(lt)) {
|
|
31444
31492
|
sanitizers.push({
|
|
31445
31493
|
type: "csharp_fullpath_startswith_guard",
|
|
31446
31494
|
method: "GetFullPath",
|
|
@@ -35060,6 +35108,443 @@ var MyBatisAnnotationSqlSinkPass = class {
|
|
|
35060
35108
|
}
|
|
35061
35109
|
};
|
|
35062
35110
|
|
|
35111
|
+
// src/analysis/passes/csharp-sanitizer-credit.ts
|
|
35112
|
+
var CSHARP_ALLOWLIST_STRIP_RE = /\bRegex\s*\.\s*Replace\s*\([^,]*,\s*@?"\[\^(?:[A-Za-z0-9_\- ]|\\w|\\d|\\s)+\]"\s*,\s*@?"[A-Za-z0-9_]?"\s*\)/;
|
|
35113
|
+
var CSHARP_ALLOWLIST_FILTER_RE = /\bnew\s+string\s*\(\s*[A-Za-z_]\w*\s*\.\s*Where\s*\(\s*char\s*\.\s*Is(?:LetterOrDigit|Digit|Letter)\s*\)\s*\.\s*ToArray\s*\(\s*\)\s*\)/;
|
|
35114
|
+
var CSHARP_SANITIZER_RES = [
|
|
35115
|
+
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
|
|
35116
|
+
// `HtmlEncoder.Default.Encode(x)` is the ASP.NET Core spelling (#520).
|
|
35117
|
+
{ re: /\bHtmlEncoder\s*\.\s*(?:Default\s*\.\s*)?Encode\s*\(/, type: "xss" },
|
|
35118
|
+
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
|
|
35119
|
+
// XML-escapes `< > & " '`. A value escaped this way cannot close the quoted
|
|
35120
|
+
// literal it is placed in, which is what XPath injection needs.
|
|
35121
|
+
{ re: /\bSecurityElement\s*\.\s*Escape\s*\(/, type: "xpath_injection" },
|
|
35122
|
+
// An allowlist strip leaves only inert characters, so it covers every family
|
|
35123
|
+
// whose exploitation needs a metacharacter. Each has a fixture; see
|
|
35124
|
+
// `issue-272-csharp-allowlist-strip.test.ts`.
|
|
35125
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
|
|
35126
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
|
|
35127
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
|
|
35128
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" },
|
|
35129
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "crlf" },
|
|
35130
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "ldap_injection" },
|
|
35131
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "xpath_injection" },
|
|
35132
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "command_injection" },
|
|
35133
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "sql_injection" },
|
|
35134
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "crlf" }
|
|
35135
|
+
];
|
|
35136
|
+
var INERT_WHEN_ALPHANUMERIC = /* @__PURE__ */ new Set([
|
|
35137
|
+
"ldap_injection",
|
|
35138
|
+
"xpath_injection",
|
|
35139
|
+
"command_injection",
|
|
35140
|
+
"sql_injection",
|
|
35141
|
+
"crlf",
|
|
35142
|
+
"xss",
|
|
35143
|
+
"path_traversal",
|
|
35144
|
+
"code_injection"
|
|
35145
|
+
]);
|
|
35146
|
+
var IDENT = "[A-Za-z_]\\w*";
|
|
35147
|
+
var ASSIGN_RE = new RegExp(`^\\s*(?:var\\s+|[A-Za-z_][\\w.<>\\[\\]?]*\\s+)?(${IDENT})\\s*=(?!=)\\s*(.+?);?\\s*$`);
|
|
35148
|
+
var ENDS_STATEMENT_RE = /[;{}]\s*(?:\/\/.*)?$/;
|
|
35149
|
+
function escapeRegex(s) {
|
|
35150
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35151
|
+
}
|
|
35152
|
+
function mentions(text, name2) {
|
|
35153
|
+
return new RegExp(`(?<![\\w])${escapeRegex(name2)}(?![\\w])`).test(text);
|
|
35154
|
+
}
|
|
35155
|
+
function insideParens(text, open) {
|
|
35156
|
+
let depth = 0;
|
|
35157
|
+
for (let i2 = open; i2 < text.length; i2++) {
|
|
35158
|
+
if (text[i2] === "(") depth++;
|
|
35159
|
+
else if (text[i2] === ")" && --depth === 0) return text.slice(open + 1, i2);
|
|
35160
|
+
}
|
|
35161
|
+
return null;
|
|
35162
|
+
}
|
|
35163
|
+
function leadingIf(text) {
|
|
35164
|
+
const m = /^\s*(?:\}\s*)?(?:else\s+)?if\s*\(/.exec(text);
|
|
35165
|
+
if (!m) return null;
|
|
35166
|
+
const open = m[0].length - 1;
|
|
35167
|
+
const cond = insideParens(text, open);
|
|
35168
|
+
if (cond === null) return null;
|
|
35169
|
+
return { cond: cond.trim(), rest: text.slice(open + cond.length + 2).trim() };
|
|
35170
|
+
}
|
|
35171
|
+
var EXITS_RE = /^(?:\{\s*)?(?:return\b|throw\b|continue\b|break\b)/;
|
|
35172
|
+
var CSharpCredit = class {
|
|
35173
|
+
constructor(code, methods) {
|
|
35174
|
+
this.methods = methods;
|
|
35175
|
+
this.lines = code.split(/\r?\n/);
|
|
35176
|
+
}
|
|
35177
|
+
methods;
|
|
35178
|
+
lines;
|
|
35179
|
+
helperCache = /* @__PURE__ */ new Map();
|
|
35180
|
+
guardHelperCache = /* @__PURE__ */ new Map();
|
|
35181
|
+
/** The innermost method containing `line`, or null. */
|
|
35182
|
+
methodAt(line) {
|
|
35183
|
+
let best = null;
|
|
35184
|
+
for (const m of this.methods) {
|
|
35185
|
+
if (line < m.start_line || line > m.end_line) continue;
|
|
35186
|
+
if (!best || m.end_line - m.start_line < best.end_line - best.start_line) best = m;
|
|
35187
|
+
}
|
|
35188
|
+
return best;
|
|
35189
|
+
}
|
|
35190
|
+
/**
|
|
35191
|
+
* Statements of lines `[from, to)`, 1-based, with a statement wrapped over
|
|
35192
|
+
* several lines joined up to its `;`. `line` is where the statement starts.
|
|
35193
|
+
*/
|
|
35194
|
+
statements(from, to) {
|
|
35195
|
+
const out2 = [];
|
|
35196
|
+
for (let i2 = Math.max(from, 1); i2 < to; i2++) {
|
|
35197
|
+
let text = (this.lines[i2 - 1] ?? "").trimEnd();
|
|
35198
|
+
const first = i2;
|
|
35199
|
+
if (/^\s*(?:[\w.<>[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !ENDS_STATEMENT_RE.test(text)) {
|
|
35200
|
+
while (i2 < to - 1 && i2 < first + 12) {
|
|
35201
|
+
i2++;
|
|
35202
|
+
text += " " + (this.lines[i2 - 1] ?? "").trim();
|
|
35203
|
+
if (ENDS_STATEMENT_RE.test(this.lines[i2 - 1] ?? "")) break;
|
|
35204
|
+
}
|
|
35205
|
+
}
|
|
35206
|
+
out2.push({ line: first, text });
|
|
35207
|
+
}
|
|
35208
|
+
return out2;
|
|
35209
|
+
}
|
|
35210
|
+
/** Does `text` apply a built-in sanitizer for `type`? */
|
|
35211
|
+
builtinSanitizes(type, text) {
|
|
35212
|
+
for (const { re, type: t } of CSHARP_SANITIZER_RES) {
|
|
35213
|
+
if (t === type && re.test(text)) return true;
|
|
35214
|
+
}
|
|
35215
|
+
if (type === "ldap_injection") {
|
|
35216
|
+
if (/\.\s*Replace\s*\(\s*"\("\s*,/.test(text) && /\.\s*Replace\s*\(\s*"\)"\s*,/.test(text)) return true;
|
|
35217
|
+
}
|
|
35218
|
+
if (type === "path_traversal") {
|
|
35219
|
+
if (/\b([A-Za-z_]\w*)\s*\.\s*StartsWith\s*\([^)]*\)\s*\?\s*\1\s*:\s*null\b/.test(text)) return true;
|
|
35220
|
+
}
|
|
35221
|
+
return false;
|
|
35222
|
+
}
|
|
35223
|
+
/**
|
|
35224
|
+
* Same-file methods that sanitize for `type`: every `return` hands back a
|
|
35225
|
+
* sanitized expression. `static string Clean(string x) { return HtmlEncode(x); }`
|
|
35226
|
+
* makes `Clean(input)` an xss sanitizer at its call sites.
|
|
35227
|
+
*/
|
|
35228
|
+
helpers(type) {
|
|
35229
|
+
const cached = this.helperCache.get(type);
|
|
35230
|
+
if (cached) return cached;
|
|
35231
|
+
const set = /* @__PURE__ */ new Set();
|
|
35232
|
+
this.helperCache.set(type, set);
|
|
35233
|
+
for (const m of this.methods) {
|
|
35234
|
+
const returns = this.returnExprs(m);
|
|
35235
|
+
if (returns.length === 0) continue;
|
|
35236
|
+
const ok = returns.every((r) => {
|
|
35237
|
+
if (this.builtinSanitizes(type, r.expr)) return true;
|
|
35238
|
+
const v = /^([A-Za-z_]\w*)$/.exec(r.expr.trim());
|
|
35239
|
+
return !!v && this.sanitizedVarsIn(type, m.start_line, r.line, false).has(v[1]);
|
|
35240
|
+
});
|
|
35241
|
+
if (ok) set.add(m.name);
|
|
35242
|
+
}
|
|
35243
|
+
return set;
|
|
35244
|
+
}
|
|
35245
|
+
/** Every returned expression of a method: `return e;` and expression bodies. */
|
|
35246
|
+
returnExprs(m) {
|
|
35247
|
+
const out2 = [];
|
|
35248
|
+
for (const st of this.statements(m.start_line, m.end_line + 1)) {
|
|
35249
|
+
for (const r of st.text.matchAll(/\breturn\s+([^;]+);/g)) out2.push({ line: st.line, expr: r[1] });
|
|
35250
|
+
if (st.line === m.start_line) {
|
|
35251
|
+
const body2 = /\)\s*=>\s*(.+?);\s*$/.exec(st.text);
|
|
35252
|
+
if (body2) out2.push({ line: st.line, expr: body2[1] });
|
|
35253
|
+
}
|
|
35254
|
+
}
|
|
35255
|
+
return out2;
|
|
35256
|
+
}
|
|
35257
|
+
/** Does `text` apply a sanitizer for `type`, built in or a same-file helper? */
|
|
35258
|
+
sanitizes(type, text) {
|
|
35259
|
+
if (this.builtinSanitizes(type, text)) return true;
|
|
35260
|
+
for (const name2 of this.helpers(type)) {
|
|
35261
|
+
if (new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`).test(text)) return true;
|
|
35262
|
+
}
|
|
35263
|
+
return false;
|
|
35264
|
+
}
|
|
35265
|
+
sanitizedVarsIn(type, from, to, withHelpers) {
|
|
35266
|
+
const sanitized = /* @__PURE__ */ new Set();
|
|
35267
|
+
for (const st of this.statements(from, to)) {
|
|
35268
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
35269
|
+
if (!m) continue;
|
|
35270
|
+
const [, lhs, rhs] = m;
|
|
35271
|
+
const credited = (withHelpers ? this.sanitizes(type, rhs) : this.builtinSanitizes(type, rhs)) || [...sanitized].some((v) => mentions(rhs, v));
|
|
35272
|
+
if (credited) sanitized.add(lhs);
|
|
35273
|
+
else sanitized.delete(lhs);
|
|
35274
|
+
}
|
|
35275
|
+
return sanitized;
|
|
35276
|
+
}
|
|
35277
|
+
/**
|
|
35278
|
+
* The variables sanitized for `type` at `sinkLine`, judged inside the method
|
|
35279
|
+
* that contains the sink (cognium-dev#579). The body is read top to bottom up
|
|
35280
|
+
* to the sink and the last assignment to a name decides: a sanitizer call or
|
|
35281
|
+
* a derivation from a sanitized name credits it, anything else removes it.
|
|
35282
|
+
* Class fields are the caller's concern: they can be sanitized in one method
|
|
35283
|
+
* and read in another.
|
|
35284
|
+
*/
|
|
35285
|
+
sanitizedVarsAt(type, sinkLine) {
|
|
35286
|
+
return this.sanitizedVarsIn(type, this.methodAt(sinkLine)?.start_line ?? 1, sinkLine, true);
|
|
35287
|
+
}
|
|
35288
|
+
/**
|
|
35289
|
+
* The text of the statement at `line` that a sink call can be in: for
|
|
35290
|
+
* `if (COND) sink(x);` that is `sink(x);`, without the condition. A name
|
|
35291
|
+
* that only appears in the condition is not something the sink receives.
|
|
35292
|
+
*/
|
|
35293
|
+
sinkText(line) {
|
|
35294
|
+
const text = this.lines[line - 1] ?? "";
|
|
35295
|
+
const guarded = leadingIf(text);
|
|
35296
|
+
return guarded && guarded.rest !== "" ? guarded.rest : text;
|
|
35297
|
+
}
|
|
35298
|
+
// -------------------------------------------------------------------------
|
|
35299
|
+
// Guards
|
|
35300
|
+
// -------------------------------------------------------------------------
|
|
35301
|
+
/** Variables a condition proves to hold only letters, digits, `_` and `-`. */
|
|
35302
|
+
allowlistVars(cond) {
|
|
35303
|
+
const out2 = [];
|
|
35304
|
+
const anchored = new RegExp(
|
|
35305
|
+
`\\bRegex\\s*\\.\\s*IsMatch\\s*\\(\\s*(${IDENT})\\s*,\\s*@?"\\^\\[(?:[A-Za-z0-9_\\- ]|\\\\w|\\\\d)+\\](?:[+*]|\\{\\d+(?:,\\d*)?\\})\\$"`,
|
|
35306
|
+
"g"
|
|
35307
|
+
);
|
|
35308
|
+
for (const m of cond.matchAll(anchored)) out2.push(m[1]);
|
|
35309
|
+
const methodGroup = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*char\\s*\\.\\s*Is(?:LetterOrDigit|Digit|Letter)\\s*\\)`, "g");
|
|
35310
|
+
for (const m of cond.matchAll(methodGroup)) out2.push(m[1]);
|
|
35311
|
+
const lambda = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*(${IDENT})\\s*=>`, "g");
|
|
35312
|
+
for (const m of cond.matchAll(lambda)) {
|
|
35313
|
+
const open = cond.indexOf("(", m.index + m[1].length);
|
|
35314
|
+
const body2 = insideParens(cond, open)?.replace(new RegExp(`^\\s*${m[2]}\\s*=>`), "") ?? "";
|
|
35315
|
+
const c = escapeRegex(m[2]);
|
|
35316
|
+
const residue = body2.replace(new RegExp(`char\\s*\\.\\s*Is(?:LetterOrDigit|Digit|Letter)\\s*\\(\\s*${c}\\s*\\)`, "g"), "").replace(new RegExp(`${c}\\s*==\\s*'[_\\-]'`, "g"), "").replace(/\|\||\s/g, "");
|
|
35317
|
+
if (residue === "") out2.push(m[1]);
|
|
35318
|
+
}
|
|
35319
|
+
return out2;
|
|
35320
|
+
}
|
|
35321
|
+
/** Variables a condition proves to be numeric (`int.TryParse(x, out _)`). */
|
|
35322
|
+
numericVars(cond, parsedFrom) {
|
|
35323
|
+
const out2 = [];
|
|
35324
|
+
const tryParse = new RegExp(
|
|
35325
|
+
`\\b(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64|Guid)\\s*\\.\\s*TryParse\\s*\\(\\s*(${IDENT})\\s*,`,
|
|
35326
|
+
"g"
|
|
35327
|
+
);
|
|
35328
|
+
for (const m of cond.matchAll(tryParse)) out2.push(m[1]);
|
|
35329
|
+
for (const [parsed, from] of parsedFrom) {
|
|
35330
|
+
if (new RegExp(`(?<![\\w])${escapeRegex(parsed)}\\s*(?:!=\\s*null|\\.\\s*HasValue)\\b`).test(cond)) out2.push(from);
|
|
35331
|
+
}
|
|
35332
|
+
return out2;
|
|
35333
|
+
}
|
|
35334
|
+
/** Variables whose host a condition compares against a constant or an allowlist. */
|
|
35335
|
+
hostVars(cond, hostOf) {
|
|
35336
|
+
const out2 = [];
|
|
35337
|
+
const compares = (expr) => {
|
|
35338
|
+
const e = escapeRegex(expr).replace(/\\ /g, "\\s*");
|
|
35339
|
+
return new RegExp(`${e}\\s*==\\s*@?"[^"]+"`).test(cond) || new RegExp(`@?"[^"]+"\\s*==\\s*${e}`).test(cond) || new RegExp(`${e}\\s*\\.\\s*Equals\\s*\\(`).test(cond) || new RegExp(`\\.\\s*Contains\\s*\\(\\s*${e}\\s*[),]`).test(cond) || new RegExp(`\\bIndexOf\\s*\\([^()]*,\\s*${e}\\s*\\)\\s*>=\\s*0`).test(cond);
|
|
35340
|
+
};
|
|
35341
|
+
const direct = new RegExp(`new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host\\b`, "g");
|
|
35342
|
+
for (const m of cond.matchAll(direct)) {
|
|
35343
|
+
if (compares(m[0])) out2.push(m[1]);
|
|
35344
|
+
}
|
|
35345
|
+
for (const [hostVar, from] of hostOf) {
|
|
35346
|
+
if (mentions(cond, hostVar) && compares(hostVar)) out2.push(from);
|
|
35347
|
+
}
|
|
35348
|
+
return out2;
|
|
35349
|
+
}
|
|
35350
|
+
/**
|
|
35351
|
+
* Same-file methods that are guards of `kind` on one of their parameters:
|
|
35352
|
+
* `static bool IsAllowed(string c) { return Array.IndexOf(Allowed, new Uri(c).Host) >= 0; }`.
|
|
35353
|
+
* Maps the method name to the position of the validated parameter.
|
|
35354
|
+
*/
|
|
35355
|
+
guardHelpers(kind) {
|
|
35356
|
+
const cached = this.guardHelperCache.get(kind);
|
|
35357
|
+
if (cached) return cached;
|
|
35358
|
+
const map = /* @__PURE__ */ new Map();
|
|
35359
|
+
this.guardHelperCache.set(kind, map);
|
|
35360
|
+
for (const m of this.methods) {
|
|
35361
|
+
const signature = this.lines[m.start_line - 1] ?? "";
|
|
35362
|
+
const open = signature.indexOf("(", signature.indexOf(m.name));
|
|
35363
|
+
const params = open === -1 ? null : insideParens(signature, open);
|
|
35364
|
+
if (!params) continue;
|
|
35365
|
+
const names = params.split(",").map((p) => p.trim().split(/\s+/).pop() ?? "");
|
|
35366
|
+
const returns = this.returnExprs(m);
|
|
35367
|
+
if (returns.length !== 1) continue;
|
|
35368
|
+
const proven = this.provenBy(kind, returns[0].expr, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), false);
|
|
35369
|
+
const pos = names.findIndex((n) => proven.includes(n));
|
|
35370
|
+
if (pos !== -1) map.set(m.name, pos);
|
|
35371
|
+
}
|
|
35372
|
+
return map;
|
|
35373
|
+
}
|
|
35374
|
+
provenBy(kind, cond, parsedFrom, hostOf, withHelpers) {
|
|
35375
|
+
const out2 = kind === "allowlist" ? this.allowlistVars(cond) : kind === "numeric" ? this.numericVars(cond, parsedFrom) : this.hostVars(cond, hostOf);
|
|
35376
|
+
if (withHelpers) {
|
|
35377
|
+
for (const [name2, pos] of this.guardHelpers(kind)) {
|
|
35378
|
+
for (const call of cond.matchAll(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`, "g"))) {
|
|
35379
|
+
const args2 = insideParens(cond, call.index + call[0].length - 1)?.split(",") ?? [];
|
|
35380
|
+
const arg = args2[pos]?.trim();
|
|
35381
|
+
if (arg && new RegExp(`^${IDENT}$`).test(arg)) out2.push(arg);
|
|
35382
|
+
}
|
|
35383
|
+
}
|
|
35384
|
+
}
|
|
35385
|
+
return out2;
|
|
35386
|
+
}
|
|
35387
|
+
/**
|
|
35388
|
+
* The variables a check dominating `sinkLine` proves safe for `type`.
|
|
35389
|
+
*
|
|
35390
|
+
* Recognised checks:
|
|
35391
|
+
* - allowlist: anchored `Regex.IsMatch(x, "^[A-Za-z0-9_]+$")`, `x.All(char.IsLetterOrDigit)`;
|
|
35392
|
+
* - numeric: `int.TryParse(x, out _)`, or `p != null` where `p = int.Parse(x)`;
|
|
35393
|
+
* - host (ssrf only): `new Uri(x).Host == "api.example.com"`, an allowlist
|
|
35394
|
+
* `Contains` / `IndexOf` of that host.
|
|
35395
|
+
* directly, through a `bool` or host variable, or through a same-file helper.
|
|
35396
|
+
*
|
|
35397
|
+
* A check dominates the sink when the sink is the body of the `if`, is
|
|
35398
|
+
* inside its block, or follows an early exit on the negated check
|
|
35399
|
+
* (`if (!ok) return;`).
|
|
35400
|
+
*/
|
|
35401
|
+
guardedVarsAt(type, sinkLine) {
|
|
35402
|
+
const kinds = [];
|
|
35403
|
+
if (INERT_WHEN_ALPHANUMERIC.has(type)) kinds.push("allowlist", "numeric");
|
|
35404
|
+
if (type === "ssrf") kinds.push("host");
|
|
35405
|
+
const proven = /* @__PURE__ */ new Set();
|
|
35406
|
+
if (kinds.length === 0) return proven;
|
|
35407
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
35408
|
+
const before = this.statements(scopeStart, sinkLine);
|
|
35409
|
+
const boolOf = /* @__PURE__ */ new Map();
|
|
35410
|
+
const parsedFrom = /* @__PURE__ */ new Map();
|
|
35411
|
+
const hostOf = /* @__PURE__ */ new Map();
|
|
35412
|
+
const parseAnywhere = new RegExp(
|
|
35413
|
+
`(${IDENT})\\s*=\\s*(?:\\([^)]*\\)\\s*)?(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64)\\s*\\.\\s*Parse\\s*\\(\\s*(${IDENT})\\s*[,)]`,
|
|
35414
|
+
"g"
|
|
35415
|
+
);
|
|
35416
|
+
for (const st of before) {
|
|
35417
|
+
for (const p of st.text.matchAll(parseAnywhere)) parsedFrom.set(p[1], p[2]);
|
|
35418
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
35419
|
+
if (!m) continue;
|
|
35420
|
+
const [, lhs, rhs] = m;
|
|
35421
|
+
const parse2 = new RegExp(`^(?:\\([^)]*\\)\\s*)?(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64)\\s*\\.\\s*Parse\\s*\\(\\s*(${IDENT})\\s*[,)]`).exec(rhs);
|
|
35422
|
+
if (parse2) parsedFrom.set(lhs, parse2[1]);
|
|
35423
|
+
const host = new RegExp(`^new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host$`).exec(rhs.trim());
|
|
35424
|
+
if (host) hostOf.set(lhs, host[1]);
|
|
35425
|
+
boolOf.set(lhs, rhs);
|
|
35426
|
+
}
|
|
35427
|
+
const credit = (cond) => {
|
|
35428
|
+
const resolved = new RegExp(`^${IDENT}$`).test(cond) ? boolOf.get(cond) ?? cond : cond;
|
|
35429
|
+
for (const kind of kinds) {
|
|
35430
|
+
for (const v of this.provenBy(kind, resolved, parsedFrom, hostOf, true)) proven.add(v);
|
|
35431
|
+
}
|
|
35432
|
+
};
|
|
35433
|
+
const own = leadingIf(this.lines[sinkLine - 1] ?? "");
|
|
35434
|
+
if (own && !own.cond.startsWith("!")) credit(own.cond);
|
|
35435
|
+
let depth = 0;
|
|
35436
|
+
for (let i2 = sinkLine - 1; i2 >= scopeStart; i2--) {
|
|
35437
|
+
const text = this.lines[i2 - 1] ?? "";
|
|
35438
|
+
for (let c = text.length - 1; c >= 0; c--) {
|
|
35439
|
+
if (text[c] === "}") depth++;
|
|
35440
|
+
else if (text[c] === "{") {
|
|
35441
|
+
if (depth > 0) {
|
|
35442
|
+
depth--;
|
|
35443
|
+
continue;
|
|
35444
|
+
}
|
|
35445
|
+
let header = text.slice(0, c);
|
|
35446
|
+
for (let h = i2 - 1; header.trim() === "" && h >= scopeStart; h--) header = this.lines[h - 1] ?? "";
|
|
35447
|
+
const enclosing = leadingIf(header);
|
|
35448
|
+
if (enclosing && !enclosing.cond.startsWith("!")) credit(enclosing.cond);
|
|
35449
|
+
}
|
|
35450
|
+
}
|
|
35451
|
+
}
|
|
35452
|
+
for (let i2 = scopeStart; i2 < sinkLine; i2++) {
|
|
35453
|
+
const guard = leadingIf(this.lines[i2 - 1] ?? "");
|
|
35454
|
+
if (!guard) continue;
|
|
35455
|
+
let rest = guard.rest;
|
|
35456
|
+
for (let n = i2 + 1; rest === "" && n < sinkLine; n++) rest = (this.lines[n - 1] ?? "").trim();
|
|
35457
|
+
if (!EXITS_RE.test(rest)) continue;
|
|
35458
|
+
if (guard.cond.startsWith("!")) {
|
|
35459
|
+
const inner = guard.cond.slice(1).trim();
|
|
35460
|
+
const wrapped = inner.startsWith("(") && insideParens(inner, 0)?.length === inner.length - 2;
|
|
35461
|
+
credit(wrapped ? inner.slice(1, -1) : inner);
|
|
35462
|
+
} else if (/!=/.test(guard.cond) && !/[|&]/.test(guard.cond)) credit(guard.cond.replace("!=", "=="));
|
|
35463
|
+
}
|
|
35464
|
+
return proven;
|
|
35465
|
+
}
|
|
35466
|
+
/**
|
|
35467
|
+
* `StringBuilder`s that hold only safe text at `sinkLine`: every write to
|
|
35468
|
+
* the builder before the sink is made of string literals and `credited`
|
|
35469
|
+
* names. One write with anything else and the builder is not credited, so
|
|
35470
|
+
* `sb.Append(validated); sb.Append(raw);` stays tainted.
|
|
35471
|
+
*
|
|
35472
|
+
* This is how a guard on a value reaches a sink that receives the builder
|
|
35473
|
+
* the value was appended to (`if (parsed != null) { sb.Append(data); Compile(sb.ToString()); }`).
|
|
35474
|
+
*/
|
|
35475
|
+
safeBuilders(credited, sinkLine) {
|
|
35476
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
35477
|
+
const onlySafe = (expr) => {
|
|
35478
|
+
const idents = expr.replace(/\$?@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ").replace(/\.\s*[A-Za-z_]\w*\s*\(\s*\)/g, " ").match(/[A-Za-z_]\w*/g) ?? [];
|
|
35479
|
+
return idents.every((id) => credited.has(id));
|
|
35480
|
+
};
|
|
35481
|
+
const state = /* @__PURE__ */ new Map();
|
|
35482
|
+
const declRe = new RegExp(`\\b(?:StringBuilder|var)\\s+(${IDENT})\\s*=\\s*new\\s+(?:System\\s*\\.\\s*Text\\s*\\.\\s*)?StringBuilder\\s*\\(`);
|
|
35483
|
+
const writeRe = new RegExp(`^\\s*(${IDENT})\\s*\\.\\s*(?:Append|AppendLine|AppendFormat|Insert)\\s*\\(`);
|
|
35484
|
+
for (const st of this.statements(scopeStart, sinkLine)) {
|
|
35485
|
+
const decl = declRe.exec(st.text);
|
|
35486
|
+
if (decl) {
|
|
35487
|
+
const args2 = insideParens(st.text, decl.index + decl[0].length - 1) ?? "";
|
|
35488
|
+
state.set(decl[1], onlySafe(args2));
|
|
35489
|
+
continue;
|
|
35490
|
+
}
|
|
35491
|
+
const write = writeRe.exec(st.text);
|
|
35492
|
+
if (write && state.has(write[1])) {
|
|
35493
|
+
const args2 = insideParens(st.text, write[0].length - 1);
|
|
35494
|
+
if (args2 === null || !onlySafe(args2)) state.set(write[1], false);
|
|
35495
|
+
}
|
|
35496
|
+
}
|
|
35497
|
+
return new Set([...state].filter(([, safe]) => safe).map(([name2]) => name2));
|
|
35498
|
+
}
|
|
35499
|
+
/**
|
|
35500
|
+
* True when what leaves on `line` has been sanitized or validated for some
|
|
35501
|
+
* sink type. Used for the untyped "tainted data reaches an external call"
|
|
35502
|
+
* fallback, which has no sink type of its own to ask about.
|
|
35503
|
+
*/
|
|
35504
|
+
creditedOnLine(line) {
|
|
35505
|
+
const text = this.sinkText(line);
|
|
35506
|
+
const types = new Set(CSHARP_SANITIZER_RES.map((r) => r.type));
|
|
35507
|
+
for (const t of INERT_WHEN_ALPHANUMERIC) types.add(t);
|
|
35508
|
+
types.add("ssrf");
|
|
35509
|
+
for (const type of types) {
|
|
35510
|
+
if (this.sanitizes(type, text)) return true;
|
|
35511
|
+
const credited = /* @__PURE__ */ new Set([...this.sanitizedVarsAt(type, line), ...this.guardedVarsAt(type, line)]);
|
|
35512
|
+
for (const v of this.safeBuilders(credited, line)) credited.add(v);
|
|
35513
|
+
for (const v of credited) if (mentions(text, v)) return true;
|
|
35514
|
+
}
|
|
35515
|
+
return this.fixedHostRequest(line);
|
|
35516
|
+
}
|
|
35517
|
+
// -------------------------------------------------------------------------
|
|
35518
|
+
// SSRF: constant host
|
|
35519
|
+
// -------------------------------------------------------------------------
|
|
35520
|
+
/**
|
|
35521
|
+
* True when the request on `sinkLine` goes to a constant host: the URL
|
|
35522
|
+
* argument starts with a string literal holding the whole authority, as in
|
|
35523
|
+
* `GetAsync("https://api.internal.example.com/?x=" + v)`. Input can then only
|
|
35524
|
+
* extend the path or query. The literal must be the first operand of the
|
|
35525
|
+
* argument, and must end the authority (`/`, `?` or `#` after the host), or
|
|
35526
|
+
* `"https://api.example.com" + x` would let `x = ".evil.org"` move the host.
|
|
35527
|
+
*/
|
|
35528
|
+
fixedHostRequest(sinkLine) {
|
|
35529
|
+
const fixedLiteral = '\\$?@?"https?:\\/\\/[A-Za-z0-9.\\-]+(?::\\d+)?[\\/?#][^"]*"';
|
|
35530
|
+
const text = this.lines[sinkLine - 1] ?? "";
|
|
35531
|
+
const argStart = `\\(\\s*(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?`;
|
|
35532
|
+
if (new RegExp(argStart + fixedLiteral).test(text)) return true;
|
|
35533
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
35534
|
+
const fixed = /* @__PURE__ */ new Set();
|
|
35535
|
+
for (const st of this.statements(scopeStart, sinkLine)) {
|
|
35536
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
35537
|
+
if (!m) continue;
|
|
35538
|
+
if (new RegExp("^(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?" + fixedLiteral).test(m[2].trim())) fixed.add(m[1]);
|
|
35539
|
+
else fixed.delete(m[1]);
|
|
35540
|
+
}
|
|
35541
|
+
for (const v of fixed) {
|
|
35542
|
+
if (new RegExp(argStart + escapeRegex(v) + "\\s*[,)]").test(text)) return true;
|
|
35543
|
+
}
|
|
35544
|
+
return false;
|
|
35545
|
+
}
|
|
35546
|
+
};
|
|
35547
|
+
|
|
35063
35548
|
// src/analysis/passes/sink-filter-pass.ts
|
|
35064
35549
|
function escapeReSf(s) {
|
|
35065
35550
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -35350,7 +35835,6 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
35350
35835
|
}
|
|
35351
35836
|
return covered;
|
|
35352
35837
|
}
|
|
35353
|
-
var CSHARP_ALLOWLIST_STRIP_RE = /\bRegex\s*\.\s*Replace\s*\([^,]*,\s*@?"\[\^(?:[A-Za-z0-9_\- ]|\\w|\\d|\\s)+\]"\s*,\s*@?"[A-Za-z0-9_]?"\s*\)/;
|
|
35354
35838
|
function isUnwrittenLocalMapKeyRead(code, source) {
|
|
35355
35839
|
if (source.type !== "plugin_param") return false;
|
|
35356
35840
|
const lines = code.split("\n");
|
|
@@ -35372,18 +35856,6 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
35372
35856
|
if (lines.some((l) => putRe.test(l))) return false;
|
|
35373
35857
|
return true;
|
|
35374
35858
|
}
|
|
35375
|
-
var CSHARP_SANITIZER_RES = [
|
|
35376
|
-
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
|
|
35377
|
-
{ re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: "xss" },
|
|
35378
|
-
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
|
|
35379
|
-
// An allowlist strip leaves only inert characters, so it covers every family
|
|
35380
|
-
// whose exploitation needs a metacharacter. Each has a fixture; see
|
|
35381
|
-
// `issue-272-csharp-allowlist-strip.test.ts`.
|
|
35382
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
|
|
35383
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
|
|
35384
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
|
|
35385
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" }
|
|
35386
|
-
];
|
|
35387
35859
|
function csharpSanitizedVarsByType(code) {
|
|
35388
35860
|
const lines = code.split("\n");
|
|
35389
35861
|
const byType = /* @__PURE__ */ new Map();
|
|
@@ -35411,7 +35883,7 @@ function csharpSanitizedVarsByType(code) {
|
|
|
35411
35883
|
for (const [type, set] of byType) {
|
|
35412
35884
|
if (set.has(lhs)) continue;
|
|
35413
35885
|
const refsSanitized = [...set].some(
|
|
35414
|
-
(v) => new RegExp(`(?<![\\w])${
|
|
35886
|
+
(v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(rhs)
|
|
35415
35887
|
);
|
|
35416
35888
|
if (refsSanitized) {
|
|
35417
35889
|
set.add(lhs);
|
|
@@ -36202,18 +36674,25 @@ var SinkFilterPass = class {
|
|
|
36202
36674
|
if (language === "csharp") {
|
|
36203
36675
|
const sourceLines = ctx.code.split("\n");
|
|
36204
36676
|
const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
|
|
36677
|
+
const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
|
|
36678
|
+
const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
|
|
36205
36679
|
const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
|
|
36206
36680
|
filtered = filtered.filter((sink) => {
|
|
36207
36681
|
if (sink.type === "xxe" && xxeHardened) return false;
|
|
36208
36682
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
36209
|
-
|
|
36210
|
-
|
|
36211
|
-
|
|
36212
|
-
const
|
|
36213
|
-
|
|
36214
|
-
|
|
36215
|
-
|
|
36216
|
-
|
|
36683
|
+
if (credit.sanitizes(sink.type, sinkLineText)) return false;
|
|
36684
|
+
if (sink.type === "ssrf" && credit.fixedHostRequest(sink.line)) return false;
|
|
36685
|
+
const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
|
|
36686
|
+
const fileWide = sanitizedByType.get(sink.type);
|
|
36687
|
+
const sanitizedVars = new Set(scoped);
|
|
36688
|
+
if (fileWide) {
|
|
36689
|
+
for (const v of fileWide) if (csFields.has(v)) sanitizedVars.add(v);
|
|
36690
|
+
}
|
|
36691
|
+
for (const v of credit.guardedVarsAt(sink.type, sink.line)) sanitizedVars.add(v);
|
|
36692
|
+
for (const v of credit.safeBuilders(sanitizedVars, sink.line)) sanitizedVars.add(v);
|
|
36693
|
+
const received = credit.sinkText(sink.line);
|
|
36694
|
+
for (const v of sanitizedVars) {
|
|
36695
|
+
if (new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(received)) return false;
|
|
36217
36696
|
}
|
|
36218
36697
|
return true;
|
|
36219
36698
|
});
|
|
@@ -36221,7 +36700,7 @@ var SinkFilterPass = class {
|
|
|
36221
36700
|
if (language === "csharp") {
|
|
36222
36701
|
const sourceLines = ctx.code.split("\n");
|
|
36223
36702
|
const constBaseClients = /* @__PURE__ */ new Set();
|
|
36224
|
-
const declRe = /\b(?:var|HttpClient)\s+([A-Za-z_]\w*)\s*=\s*new\s+HttpClient\s*\{[^}]*\bBaseAddress\s*=\s*new\s+Uri\s*\(\s*(?:@?"[^"]*"|\$?"[^"{}]*")\s*\)/;
|
|
36703
|
+
const declRe = /\b(?:var|HttpClient)\s+([A-Za-z_]\w*)\s*=\s*new\s+HttpClient\s*(?:\([^)]*\)\s*)?\{[^}]*\bBaseAddress\s*=\s*new\s+(?:System\s*\.\s*)?Uri\s*\(\s*(?:@?"[^"]*"|\$?"[^"{}]*")\s*\)/;
|
|
36225
36704
|
for (const line of sourceLines) {
|
|
36226
36705
|
const m = declRe.exec(line);
|
|
36227
36706
|
if (m) constBaseClients.add(m[1]);
|
|
@@ -36233,13 +36712,20 @@ var SinkFilterPass = class {
|
|
|
36233
36712
|
const text = sourceLines[sink.line - 1] ?? "";
|
|
36234
36713
|
let onConstClient = false;
|
|
36235
36714
|
for (const v of constBaseClients) {
|
|
36236
|
-
if (new RegExp(`(?<![\\w])${
|
|
36715
|
+
if (new RegExp(`(?<![\\w])${escapeRegex2(v)}\\s*\\.`).test(text)) {
|
|
36237
36716
|
onConstClient = true;
|
|
36238
36717
|
break;
|
|
36239
36718
|
}
|
|
36240
36719
|
}
|
|
36241
36720
|
if (!onConstClient) return true;
|
|
36242
|
-
|
|
36721
|
+
let m = relativeLiteralArgRe.exec(text);
|
|
36722
|
+
if (!m) {
|
|
36723
|
+
const arg = /Async\s*\(\s*([A-Za-z_]\w*)\s*[,)]/.exec(text)?.[1];
|
|
36724
|
+
if (arg) {
|
|
36725
|
+
const assigned = new RegExp(`\\b${escapeRegex2(arg)}\\s*=\\s*(?:@?\\$?)"([^"]*)"`);
|
|
36726
|
+
for (let i2 = sink.line - 2; i2 >= 0 && !m; i2--) m = assigned.exec(sourceLines[i2] ?? "");
|
|
36727
|
+
}
|
|
36728
|
+
}
|
|
36243
36729
|
if (!m) return true;
|
|
36244
36730
|
const lit = m[1];
|
|
36245
36731
|
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(lit)) return true;
|
|
@@ -36269,7 +36755,7 @@ var SinkFilterPass = class {
|
|
|
36269
36755
|
const allLdapSanitizers = [...PY_BUILTIN_LDAP_SANITIZERS, ...wrappers];
|
|
36270
36756
|
if (allLdapSanitizers.length > 0) {
|
|
36271
36757
|
const ldapPatterns = allLdapSanitizers.map(
|
|
36272
|
-
(name2) => new RegExp(`\\b${
|
|
36758
|
+
(name2) => new RegExp(`\\b${escapeRegex2(name2)}\\s*\\(`)
|
|
36273
36759
|
);
|
|
36274
36760
|
filtered = filtered.filter((sink) => {
|
|
36275
36761
|
if (sink.type !== "ldap_injection") return true;
|
|
@@ -36341,7 +36827,7 @@ var SinkFilterPass = class {
|
|
|
36341
36827
|
return { sources, sinks: filtered, sanitizers };
|
|
36342
36828
|
}
|
|
36343
36829
|
};
|
|
36344
|
-
function
|
|
36830
|
+
function escapeRegex2(s) {
|
|
36345
36831
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36346
36832
|
}
|
|
36347
36833
|
var RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
@@ -36425,7 +36911,7 @@ function extractSourceIdentifiers(text) {
|
|
|
36425
36911
|
return out2;
|
|
36426
36912
|
}
|
|
36427
36913
|
function isAssignedFromSanitizerPattern(sourceLines, sinkLine, varName, sanitizerPatterns, lookback = 30) {
|
|
36428
|
-
const escapedVar =
|
|
36914
|
+
const escapedVar = escapeRegex2(varName);
|
|
36429
36915
|
const assignRe = new RegExp(
|
|
36430
36916
|
`(?:\\b(?:const|let|var|final|String)\\s+)?\\b${escapedVar}\\s*=\\s*(.+)`
|
|
36431
36917
|
);
|
|
@@ -37895,7 +38381,7 @@ var LibraryProfileXssGatePass = class {
|
|
|
37895
38381
|
|
|
37896
38382
|
// src/analysis/passes/taint-propagation-pass.ts
|
|
37897
38383
|
var CONTAINER_READ_SOURCES = /* @__PURE__ */ new Set(["plugin_param", "config_param"]);
|
|
37898
|
-
function makeConstPropVeto(constProp, graph, sources, code) {
|
|
38384
|
+
function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
37899
38385
|
const lines = typeof code === "string" ? code.split("\n") : [];
|
|
37900
38386
|
const selfReassign = /* @__PURE__ */ new Map();
|
|
37901
38387
|
const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -37925,6 +38411,7 @@ function makeConstPropVeto(constProp, graph, sources, code) {
|
|
|
37925
38411
|
const r = isFalsePositive(constProp, step.line, step.variable);
|
|
37926
38412
|
if (!r.isFalsePositive) return false;
|
|
37927
38413
|
if (r.reason !== "variable_not_tainted") return true;
|
|
38414
|
+
if (language === "csharp") return false;
|
|
37928
38415
|
const lo = Math.min(sourceLine, sinkLine);
|
|
37929
38416
|
let d = defAt(step.variable, step.line);
|
|
37930
38417
|
let selfHops = 0;
|
|
@@ -37958,7 +38445,7 @@ var TaintPropagationPass = class {
|
|
|
37958
38445
|
return { flows: [] };
|
|
37959
38446
|
}
|
|
37960
38447
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
37961
|
-
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
38448
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
|
|
37962
38449
|
const verifiedFlows = propagationResult.flows.filter((flow) => {
|
|
37963
38450
|
if (constProp.unreachableLines.has(flow.sink.line)) return false;
|
|
37964
38451
|
for (const step of flow.path) {
|
|
@@ -38457,7 +38944,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
38457
38944
|
if (source.variable && source.variable !== varName && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
38458
38945
|
continue;
|
|
38459
38946
|
}
|
|
38460
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
|
|
38947
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line, unreachableLines)) {
|
|
38461
38948
|
continue;
|
|
38462
38949
|
}
|
|
38463
38950
|
flows.push({
|
|
@@ -38497,7 +38984,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
38497
38984
|
if (source.variable && source.variable !== collectionVar && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
38498
38985
|
continue;
|
|
38499
38986
|
}
|
|
38500
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
|
|
38987
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line, unreachableLines)) {
|
|
38501
38988
|
continue;
|
|
38502
38989
|
}
|
|
38503
38990
|
flows.push({
|
|
@@ -38611,7 +39098,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
38611
39098
|
if (paramSource) {
|
|
38612
39099
|
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
38613
39100
|
if (!exists) {
|
|
38614
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
39101
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line, unreachableLines)) {
|
|
38615
39102
|
continue;
|
|
38616
39103
|
}
|
|
38617
39104
|
flows.push({
|
|
@@ -38635,6 +39122,85 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
38635
39122
|
void types;
|
|
38636
39123
|
return flows;
|
|
38637
39124
|
}
|
|
39125
|
+
function csharpMethodScopedAliases(code, seeds, methods, fieldNames) {
|
|
39126
|
+
const lines = code.split("\n");
|
|
39127
|
+
const methodOf = (line) => {
|
|
39128
|
+
let best = -1;
|
|
39129
|
+
let span = Infinity;
|
|
39130
|
+
methods.forEach((m, i2) => {
|
|
39131
|
+
if (line >= m.start_line && line <= m.end_line && m.end_line - m.start_line < span) {
|
|
39132
|
+
best = i2;
|
|
39133
|
+
span = m.end_line - m.start_line;
|
|
39134
|
+
}
|
|
39135
|
+
});
|
|
39136
|
+
return best;
|
|
39137
|
+
};
|
|
39138
|
+
const declRe = /^\s*(?:public|private|protected|internal|static|readonly|const|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*\??)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
39139
|
+
const assignRe = /^\s*(?:this\s*\.\s*)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
39140
|
+
const returnRe = /^\s*return\s+(.+?);\s*$/;
|
|
39141
|
+
const KEYWORDS2 = /* @__PURE__ */ new Set(["return", "throw", "new", "await", "yield", "else", "case", "using", "var"]);
|
|
39142
|
+
const codeOf = (rhs) => rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
|
|
39143
|
+
const local = /* @__PURE__ */ new Map();
|
|
39144
|
+
const taintedFields = /* @__PURE__ */ new Set();
|
|
39145
|
+
const returnsTainted = /* @__PURE__ */ new Set();
|
|
39146
|
+
const localsOf = (m) => {
|
|
39147
|
+
let set = local.get(m);
|
|
39148
|
+
if (!set) {
|
|
39149
|
+
set = /* @__PURE__ */ new Set();
|
|
39150
|
+
local.set(m, set);
|
|
39151
|
+
}
|
|
39152
|
+
return set;
|
|
39153
|
+
};
|
|
39154
|
+
for (const seed of seeds) {
|
|
39155
|
+
const base = seed.variable.replace(/^this\./, "");
|
|
39156
|
+
if (fieldNames.has(base)) taintedFields.add(base);
|
|
39157
|
+
else localsOf(methodOf(seed.line)).add(seed.variable);
|
|
39158
|
+
}
|
|
39159
|
+
const out2 = [];
|
|
39160
|
+
let changed = true;
|
|
39161
|
+
for (let guard = 0; changed && guard < lines.length + 2; guard++) {
|
|
39162
|
+
changed = false;
|
|
39163
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
39164
|
+
let text = lines[i2];
|
|
39165
|
+
const trimmed = text.trimStart();
|
|
39166
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
39167
|
+
if (/^\s*(?:[\w.<>\[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !/;\s*(?:\/\/.*)?$/.test(text)) {
|
|
39168
|
+
for (let j = i2 + 1; j < lines.length && j <= i2 + 12; j++) {
|
|
39169
|
+
text = text.trimEnd() + " " + lines[j].trim();
|
|
39170
|
+
if (/;\s*(?:\/\/.*)?$/.test(lines[j])) break;
|
|
39171
|
+
}
|
|
39172
|
+
}
|
|
39173
|
+
const m = methodOf(i2 + 1);
|
|
39174
|
+
const here = localsOf(m);
|
|
39175
|
+
const ret = returnRe.exec(text);
|
|
39176
|
+
if (ret && m !== -1 && !returnsTainted.has(methods[m].name)) {
|
|
39177
|
+
const idents2 = codeOf(ret[1]).match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
39178
|
+
if (idents2.some((t) => here.has(t) || taintedFields.has(t))) {
|
|
39179
|
+
returnsTainted.add(methods[m].name);
|
|
39180
|
+
changed = true;
|
|
39181
|
+
}
|
|
39182
|
+
continue;
|
|
39183
|
+
}
|
|
39184
|
+
const d = declRe.exec(text) ?? assignRe.exec(text);
|
|
39185
|
+
if (!d) continue;
|
|
39186
|
+
const [, lhs, rhs] = d;
|
|
39187
|
+
if (KEYWORDS2.has(lhs)) continue;
|
|
39188
|
+
const isField = fieldNames.has(lhs) && !declRe.test(text);
|
|
39189
|
+
if (isField ? taintedFields.has(lhs) : here.has(lhs)) continue;
|
|
39190
|
+
const rhsCode = codeOf(rhs);
|
|
39191
|
+
const idents = rhsCode.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
39192
|
+
const viaLocal = idents.some((t) => here.has(t));
|
|
39193
|
+
const viaField = idents.some((t) => taintedFields.has(t) && !here.has(t));
|
|
39194
|
+
const viaCall = [...rhsCode.matchAll(/([\p{L}_][\p{L}\p{N}_]*)\s*\(/gu)].some((c) => returnsTainted.has(c[1]));
|
|
39195
|
+
if (!viaLocal && !viaField && !viaCall) continue;
|
|
39196
|
+
if (isField) taintedFields.add(lhs);
|
|
39197
|
+
else here.add(lhs);
|
|
39198
|
+
out2.push({ variable: lhs, line: i2 + 1, carried: !viaLocal });
|
|
39199
|
+
changed = true;
|
|
39200
|
+
}
|
|
39201
|
+
}
|
|
39202
|
+
return out2;
|
|
39203
|
+
}
|
|
38638
39204
|
var _reassignSplitCode;
|
|
38639
39205
|
var _reassignSplitLines;
|
|
38640
39206
|
function splitLinesCached(code) {
|
|
@@ -38643,7 +39209,7 @@ function splitLinesCached(code) {
|
|
|
38643
39209
|
_reassignSplitLines = code.split("\n");
|
|
38644
39210
|
return _reassignSplitLines;
|
|
38645
39211
|
}
|
|
38646
|
-
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
39212
|
+
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
|
|
38647
39213
|
if (!variable || sinkLine - srcLine < 2) return false;
|
|
38648
39214
|
if (!/^[A-Za-z_][\w]*$/.test(variable)) return false;
|
|
38649
39215
|
const lines = splitLinesCached(code);
|
|
@@ -38664,40 +39230,103 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
|
38664
39230
|
if (!line) continue;
|
|
38665
39231
|
if (reGuarded.test(line)) return true;
|
|
38666
39232
|
if (!reNaked.test(line) && !reSwitchCase.test(line)) continue;
|
|
38667
|
-
if (
|
|
39233
|
+
if (siblingBranchAssignsNonLiteral(lines, i2, variable, strLit, unreachableLines)) continue;
|
|
38668
39234
|
return true;
|
|
38669
39235
|
}
|
|
38670
39236
|
return false;
|
|
38671
39237
|
}
|
|
38672
|
-
function
|
|
39238
|
+
function enclosingBrace(lines, line, col) {
|
|
38673
39239
|
let depth = 0;
|
|
38674
|
-
let
|
|
38675
|
-
|
|
38676
|
-
|
|
38677
|
-
|
|
38678
|
-
if (
|
|
38679
|
-
|
|
39240
|
+
for (let i2 = line; i2 >= 0; i2--) {
|
|
39241
|
+
const text = lines[i2];
|
|
39242
|
+
for (let c = i2 === line ? Math.min(col, text.length) - 1 : text.length - 1; c >= 0; c--) {
|
|
39243
|
+
if (text[c] === "}") depth++;
|
|
39244
|
+
else if (text[c] === "{") {
|
|
39245
|
+
if (depth === 0) {
|
|
39246
|
+
let header = text.slice(0, c).trim();
|
|
39247
|
+
let headerLine = i2;
|
|
39248
|
+
for (let h = i2 - 1; header === "" && h >= 0; h--) {
|
|
39249
|
+
header = lines[h].trim();
|
|
39250
|
+
headerLine = h;
|
|
39251
|
+
}
|
|
39252
|
+
return { line: i2, col: c, header, headerLine };
|
|
39253
|
+
}
|
|
39254
|
+
depth--;
|
|
39255
|
+
}
|
|
38680
39256
|
}
|
|
38681
|
-
|
|
38682
|
-
|
|
38683
|
-
|
|
39257
|
+
}
|
|
39258
|
+
return null;
|
|
39259
|
+
}
|
|
39260
|
+
function matchingClose(lines, line, col) {
|
|
39261
|
+
let depth = 0;
|
|
39262
|
+
for (let i2 = line; i2 < lines.length; i2++) {
|
|
39263
|
+
const text = lines[i2];
|
|
39264
|
+
for (let c = i2 === line ? col : 0; c < text.length; c++) {
|
|
39265
|
+
if (text[c] === "{") depth++;
|
|
39266
|
+
else if (text[c] === "}" && --depth === 0) return { line: i2, col: c };
|
|
38684
39267
|
}
|
|
38685
39268
|
}
|
|
38686
|
-
|
|
39269
|
+
return null;
|
|
39270
|
+
}
|
|
39271
|
+
function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachableLines) {
|
|
39272
|
+
let block = enclosingBrace(lines, idx, 0);
|
|
39273
|
+
if (block && /^(?:case\b.*|default\s*):$/.test(block.header)) {
|
|
39274
|
+
block = enclosingBrace(lines, block.line, block.col);
|
|
39275
|
+
}
|
|
39276
|
+
if (!block) return false;
|
|
39277
|
+
let first = block.headerLine;
|
|
39278
|
+
let last;
|
|
39279
|
+
const end = matchingClose(lines, block.line, block.col);
|
|
39280
|
+
if (!end) return false;
|
|
39281
|
+
last = end.line;
|
|
39282
|
+
if (/\bswitch\b/.test(block.header)) {
|
|
39283
|
+
} else if (/^(?:\}\s*)?else\b/.test(block.header) || /^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(block.header)) {
|
|
39284
|
+
let cur = block;
|
|
39285
|
+
for (let guard = 0; guard < 64 && /^(?:\}\s*)?else\b/.test(cur.header); guard++) {
|
|
39286
|
+
let closeLine = cur.headerLine;
|
|
39287
|
+
let closeCol = lines[closeLine].indexOf("}");
|
|
39288
|
+
if (closeCol === -1 || closeCol > lines[closeLine].search(/\belse\b/)) {
|
|
39289
|
+
closeLine--;
|
|
39290
|
+
while (closeLine >= 0 && lines[closeLine].trim() === "") closeLine--;
|
|
39291
|
+
if (closeLine < 0 || !lines[closeLine].trimEnd().endsWith("}")) return false;
|
|
39292
|
+
closeCol = lines[closeLine].lastIndexOf("}");
|
|
39293
|
+
}
|
|
39294
|
+
const prev = enclosingBrace(lines, closeLine, closeCol);
|
|
39295
|
+
if (!prev) return false;
|
|
39296
|
+
cur = prev;
|
|
39297
|
+
first = prev.headerLine;
|
|
39298
|
+
}
|
|
39299
|
+
let close = end;
|
|
39300
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
39301
|
+
let l = close.line;
|
|
39302
|
+
let rest = lines[l].slice(close.col + 1).trim();
|
|
39303
|
+
while (rest === "" && l + 1 < lines.length) {
|
|
39304
|
+
l++;
|
|
39305
|
+
rest = lines[l].trim();
|
|
39306
|
+
}
|
|
39307
|
+
if (!/^else\b/.test(rest)) break;
|
|
39308
|
+
let openLine = l;
|
|
39309
|
+
let openCol = lines[l].indexOf("{", l === close.line ? close.col + 1 : 0);
|
|
39310
|
+
if (openCol === -1) {
|
|
39311
|
+
openLine = l + 1;
|
|
39312
|
+
while (openLine < lines.length && lines[openLine].trim() === "") openLine++;
|
|
39313
|
+
if (openLine >= lines.length || !lines[openLine].trimStart().startsWith("{")) break;
|
|
39314
|
+
openCol = lines[openLine].indexOf("{");
|
|
39315
|
+
}
|
|
39316
|
+
const next = matchingClose(lines, openLine, openCol);
|
|
39317
|
+
if (!next) break;
|
|
39318
|
+
close = next;
|
|
39319
|
+
last = next.line;
|
|
39320
|
+
}
|
|
39321
|
+
} else {
|
|
39322
|
+
return false;
|
|
39323
|
+
}
|
|
38687
39324
|
const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
|
|
38688
39325
|
const literalOnly = new RegExp(`^${strLit}$`);
|
|
38689
|
-
|
|
38690
|
-
|
|
38691
|
-
const
|
|
38692
|
-
if (
|
|
38693
|
-
const m = assign.exec(line);
|
|
38694
|
-
if (m && m[1] && !literalOnly.test(m[1])) return true;
|
|
38695
|
-
}
|
|
38696
|
-
for (const ch of line) {
|
|
38697
|
-
if (ch === "{") depth++;
|
|
38698
|
-
else if (ch === "}") depth--;
|
|
38699
|
-
}
|
|
38700
|
-
if (i2 > start2 && depth <= 0) break;
|
|
39326
|
+
for (let i2 = first; i2 <= last; i2++) {
|
|
39327
|
+
if (i2 === idx || unreachableLines?.has(i2 + 1)) continue;
|
|
39328
|
+
const m = assign.exec(lines[i2]);
|
|
39329
|
+
if (m && m[1] && !literalOnly.test(m[1])) return true;
|
|
38701
39330
|
}
|
|
38702
39331
|
return false;
|
|
38703
39332
|
}
|
|
@@ -38825,6 +39454,18 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38825
39454
|
const goFnStart = (line) => enclosingStart(goFnRanges, line);
|
|
38826
39455
|
const csFnRanges = methodRanges(["csharp"]);
|
|
38827
39456
|
const csFnStart = (line) => enclosingStart(csFnRanges, line);
|
|
39457
|
+
const csFieldNames = /* @__PURE__ */ new Set();
|
|
39458
|
+
if (language === "csharp") {
|
|
39459
|
+
for (const t of types ?? []) for (const f of t.fields ?? []) csFieldNames.add(f.name);
|
|
39460
|
+
}
|
|
39461
|
+
const csDifferentMethod = (variable, sourceLine, otherLine) => {
|
|
39462
|
+
if (csFnRanges.length === 0) return false;
|
|
39463
|
+
const a = csFnStart(sourceLine);
|
|
39464
|
+
const b = csFnStart(otherLine);
|
|
39465
|
+
if (a === -1 || b === -1 || a === b) return false;
|
|
39466
|
+
const base = variable.replace(/^this\./, "").split(".")[0];
|
|
39467
|
+
return !csFieldNames.has(base);
|
|
39468
|
+
};
|
|
38828
39469
|
const sourcesWithVar = sources.filter(
|
|
38829
39470
|
(s) => typeof s.variable === "string" && s.variable.length > 0
|
|
38830
39471
|
);
|
|
@@ -39072,7 +39713,28 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39072
39713
|
}
|
|
39073
39714
|
}
|
|
39074
39715
|
}
|
|
39075
|
-
if (
|
|
39716
|
+
if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0 && csFnRanges.length > 0) {
|
|
39717
|
+
const csMethods = (types ?? []).flatMap((t) => t.methods);
|
|
39718
|
+
const seeds = sourcesWithVar.map((sv) => ({ variable: sv.variable, line: sv.line }));
|
|
39719
|
+
const methodStartOf = (line) => csFnStart(line);
|
|
39720
|
+
const aliases = csharpMethodScopedAliases(code, seeds, csMethods, csFieldNames);
|
|
39721
|
+
const fileAnchor = sourcesWithVar.reduce((a, sv) => sv.line < a.line ? sv : a, sourcesWithVar[0]);
|
|
39722
|
+
const have = new Set(sourcesWithVar.map((sv) => `${sv.variable}@${methodStartOf(sv.line)}`));
|
|
39723
|
+
for (const alias of aliases) {
|
|
39724
|
+
const fn = methodStartOf(alias.line);
|
|
39725
|
+
const isField = csFieldNames.has(alias.variable);
|
|
39726
|
+
const key = `${alias.variable}@${isField ? "field" : fn}`;
|
|
39727
|
+
if (have.has(key)) continue;
|
|
39728
|
+
have.add(key);
|
|
39729
|
+
const inMethod = sourcesWithVar.filter((sv) => methodStartOf(sv.line) === fn && sv.line <= alias.line);
|
|
39730
|
+
const from = !alias.carried && inMethod.length > 0 ? inMethod.reduce((a, sv) => sv.line < a.line ? sv : a, inMethod[0]) : fileAnchor;
|
|
39731
|
+
sourcesWithVar.push({
|
|
39732
|
+
...from,
|
|
39733
|
+
variable: alias.variable,
|
|
39734
|
+
...alias.carried || inMethod.length === 0 ? { line: alias.line } : {}
|
|
39735
|
+
});
|
|
39736
|
+
}
|
|
39737
|
+
} else if ((language === "java" || language === "csharp") && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
39076
39738
|
const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
39077
39739
|
const derived = buildJavaTaintedVars(code, seedVars);
|
|
39078
39740
|
if (derived.size > 0) {
|
|
@@ -39114,20 +39776,25 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39114
39776
|
if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
39115
39777
|
const lines = code.split("\n");
|
|
39116
39778
|
const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
|
|
39117
|
-
const
|
|
39779
|
+
const seeded = /* @__PURE__ */ new Set();
|
|
39118
39780
|
let added = true;
|
|
39119
39781
|
let guard = 0;
|
|
39120
39782
|
while (added && guard < lines.length + 2) {
|
|
39121
39783
|
added = false;
|
|
39122
39784
|
guard++;
|
|
39123
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
39124
39785
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
39125
39786
|
const m = ctRe.exec(lines[i2]);
|
|
39126
39787
|
if (!m) continue;
|
|
39127
39788
|
const [, obj, rhs] = m;
|
|
39128
|
-
|
|
39129
|
-
if (
|
|
39130
|
-
sourcesWithVar.
|
|
39789
|
+
const key = `${obj}@${csFnStart(i2 + 1)}`;
|
|
39790
|
+
if (seeded.has(key)) continue;
|
|
39791
|
+
const reachable = sourcesWithVar.filter(
|
|
39792
|
+
(sv) => sv.line <= i2 + 1 && !csDifferentMethod(sv.variable, sv.line, i2 + 1)
|
|
39793
|
+
);
|
|
39794
|
+
if (reachable.length === 0 || !argsReferenceTaintedVar(rhs, reachable)) continue;
|
|
39795
|
+
const from = reachable.reduce((a, sv) => sv.line < a.line ? sv : a, reachable[0]);
|
|
39796
|
+
sourcesWithVar.push({ ...from, variable: obj, line: i2 + 1 });
|
|
39797
|
+
seeded.add(key);
|
|
39131
39798
|
added = true;
|
|
39132
39799
|
}
|
|
39133
39800
|
}
|
|
@@ -39143,17 +39810,18 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39143
39810
|
if (builders.size > 0) {
|
|
39144
39811
|
const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
|
|
39145
39812
|
const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
|
|
39813
|
+
const builderSeeded = /* @__PURE__ */ new Set();
|
|
39146
39814
|
let added = true;
|
|
39147
39815
|
let guard = 0;
|
|
39148
39816
|
while (added && guard < lines.length + 2) {
|
|
39149
39817
|
added = false;
|
|
39150
39818
|
guard++;
|
|
39151
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
39152
39819
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
39153
39820
|
const m = appendRe.exec(lines[i2]);
|
|
39154
39821
|
if (!m) continue;
|
|
39155
39822
|
const [, obj, args2] = m;
|
|
39156
|
-
|
|
39823
|
+
const seedKey = `${obj}@${csFnStart(i2 + 1)}`;
|
|
39824
|
+
if (!builders.has(obj) || builderSeeded.has(seedKey)) continue;
|
|
39157
39825
|
const appendFn = csFnStart(i2 + 1);
|
|
39158
39826
|
const reachable = sourcesWithVar.filter(
|
|
39159
39827
|
(sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn
|
|
@@ -39161,6 +39829,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39161
39829
|
if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
|
|
39162
39830
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
|
|
39163
39831
|
builderSeeds.add(`${obj}@${i2 + 1}`);
|
|
39832
|
+
builderSeeded.add(seedKey);
|
|
39164
39833
|
added = true;
|
|
39165
39834
|
}
|
|
39166
39835
|
}
|
|
@@ -39234,6 +39903,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39234
39903
|
const sinkFn = csFnStart(sink.line);
|
|
39235
39904
|
if (srcFn !== sinkFn) continue;
|
|
39236
39905
|
}
|
|
39906
|
+
if (csDifferentMethod(source.variable, source.line, sink.line)) continue;
|
|
39237
39907
|
if (complex) {
|
|
39238
39908
|
const re = reCache.get(source.variable);
|
|
39239
39909
|
if (!re || !re.test(expr)) continue;
|
|
@@ -39244,7 +39914,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39244
39914
|
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
39245
39915
|
break;
|
|
39246
39916
|
}
|
|
39247
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
|
|
39917
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line, unreachableLines)) {
|
|
39248
39918
|
break;
|
|
39249
39919
|
}
|
|
39250
39920
|
flows.push({
|
|
@@ -39380,6 +40050,7 @@ var InterproceduralPass = class {
|
|
|
39380
40050
|
}
|
|
39381
40051
|
}
|
|
39382
40052
|
const language = graph.ir.meta.language;
|
|
40053
|
+
const csCredit = language === "csharp" && typeof ctx.code === "string" ? new CSharpCredit(ctx.code, graph.ir.types.flatMap((t) => t.methods)) : null;
|
|
39383
40054
|
if (sinks.length > 0) {
|
|
39384
40055
|
const interProc = analyzeInterprocedural(graph, sources, sinks, sanitizers, {
|
|
39385
40056
|
taintedVariables: constProp.tainted
|
|
@@ -39406,6 +40077,9 @@ var InterproceduralPass = class {
|
|
|
39406
40077
|
if (!method) continue;
|
|
39407
40078
|
if (sink.line < method.startLine || sink.line > method.endLine) continue;
|
|
39408
40079
|
if (sanitizerMethodNames.has(method.name)) continue;
|
|
40080
|
+
if (language === "csharp" && typeof ctx.code === "string" && !csharpParamReachesSink(ctx.code, method, edge.taintedArgs, sink, csCredit)) {
|
|
40081
|
+
continue;
|
|
40082
|
+
}
|
|
39409
40083
|
for (const source of sources) {
|
|
39410
40084
|
if (source.line > edge.callLine) continue;
|
|
39411
40085
|
if (source.type === "interprocedural_param" && source.confidence < 0.6) continue;
|
|
@@ -39548,9 +40222,53 @@ var InterproceduralPass = class {
|
|
|
39548
40222
|
return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
|
|
39549
40223
|
});
|
|
39550
40224
|
}
|
|
40225
|
+
if (csCredit) {
|
|
40226
|
+
const credited = /* @__PURE__ */ new Set();
|
|
40227
|
+
filteredAdditionalFlows = filteredAdditionalFlows.filter((f) => {
|
|
40228
|
+
if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line)) return true;
|
|
40229
|
+
credited.add(f.sink_line);
|
|
40230
|
+
return false;
|
|
40231
|
+
});
|
|
40232
|
+
filteredAdditionalSinks = filteredAdditionalSinks.filter(
|
|
40233
|
+
(s) => !(s.type === "external_taint_escape" && credited.has(s.line))
|
|
40234
|
+
);
|
|
40235
|
+
}
|
|
39551
40236
|
return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
|
|
39552
40237
|
}
|
|
39553
40238
|
};
|
|
40239
|
+
function csharpParamReachesSink(code, method, taintedArgs, sink, credit) {
|
|
40240
|
+
const lines = code.split(/\r?\n/);
|
|
40241
|
+
const params = taintedArgs.length > 0 ? method.parameters.filter((p) => taintedArgs.includes(p.position)) : method.parameters;
|
|
40242
|
+
if (params.length === 0) return true;
|
|
40243
|
+
const derived = new Set(params.map((p) => p.name));
|
|
40244
|
+
const codeOf = (text) => text.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
|
|
40245
|
+
const mentionsDerived = (text) => {
|
|
40246
|
+
const bare = codeOf(text);
|
|
40247
|
+
return [...derived].filter((v) => new RegExp(`(?<![\\w.])${v}(?![\\w])`).test(bare));
|
|
40248
|
+
};
|
|
40249
|
+
const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>[\]?]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.+)$/;
|
|
40250
|
+
const objectWriteRe = /^\s*([A-Za-z_]\w*)\s*\.\s*[A-Za-z_]\w*\s*(?:\+?=(?!=)|\()(.*)$/;
|
|
40251
|
+
for (let i2 = method.startLine + 1; i2 < sink.line; i2++) {
|
|
40252
|
+
const text = lines[i2 - 1] ?? "";
|
|
40253
|
+
const assign = assignRe.exec(text);
|
|
40254
|
+
if (assign) {
|
|
40255
|
+
if (mentionsDerived(assign[2]).length > 0) derived.add(assign[1]);
|
|
40256
|
+
else if (!params.some((p) => p.name === assign[1])) derived.delete(assign[1]);
|
|
40257
|
+
continue;
|
|
40258
|
+
}
|
|
40259
|
+
const write = objectWriteRe.exec(text);
|
|
40260
|
+
if (write && mentionsDerived(write[2]).length > 0) derived.add(write[1]);
|
|
40261
|
+
}
|
|
40262
|
+
let statement = lines[sink.line - 1] ?? "";
|
|
40263
|
+
for (let i2 = sink.line; i2 < lines.length && i2 < sink.line + 6 && !/[;{}]\s*$/.test(statement.trimEnd()); i2++) {
|
|
40264
|
+
statement += " " + (lines[i2] ?? "").trim();
|
|
40265
|
+
}
|
|
40266
|
+
const reaching = mentionsDerived(statement);
|
|
40267
|
+
if (reaching.length === 0) return false;
|
|
40268
|
+
if (!credit) return true;
|
|
40269
|
+
const safe = /* @__PURE__ */ new Set([...credit.sanitizedVarsAt(sink.type, sink.line), ...credit.guardedVarsAt(sink.type, sink.line)]);
|
|
40270
|
+
return reaching.some((v) => !safe.has(v));
|
|
40271
|
+
}
|
|
39554
40272
|
|
|
39555
40273
|
// src/analysis/passes/dead-code-pass.ts
|
|
39556
40274
|
var DeadCodePass = class {
|
|
@@ -40234,7 +40952,7 @@ var NullDerefPass = class {
|
|
|
40234
40952
|
};
|
|
40235
40953
|
|
|
40236
40954
|
// src/analysis/passes/resource-leak-pass.ts
|
|
40237
|
-
function
|
|
40955
|
+
function escapeRegex3(s) {
|
|
40238
40956
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40239
40957
|
}
|
|
40240
40958
|
var FACTORY_METHOD_NAME_RE = /^(?:open|create|new|get|make|build)[A-Z]/;
|
|
@@ -40453,7 +41171,7 @@ var ResourceLeakPass = class {
|
|
|
40453
41171
|
* ownership to the caller (caller is responsible for close).
|
|
40454
41172
|
*/
|
|
40455
41173
|
isReturnedToCaller(lines, variable, fromLine, toLine) {
|
|
40456
|
-
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${
|
|
41174
|
+
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex3(variable)}\\b`);
|
|
40457
41175
|
for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
|
|
40458
41176
|
if (varRe.test(lines[l - 1] ?? "")) return true;
|
|
40459
41177
|
}
|
|
@@ -40466,7 +41184,7 @@ var ResourceLeakPass = class {
|
|
|
40466
41184
|
*/
|
|
40467
41185
|
fieldStoredName(lines, variable, openLine, toLine) {
|
|
40468
41186
|
const thisAssignRe = new RegExp(
|
|
40469
|
-
`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${
|
|
41187
|
+
`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`
|
|
40470
41188
|
);
|
|
40471
41189
|
for (let l = openLine; l <= toLine && l <= lines.length; l++) {
|
|
40472
41190
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|
|
@@ -40541,7 +41259,7 @@ var ResourceLeakPass = class {
|
|
|
40541
41259
|
candidateField = variable;
|
|
40542
41260
|
} else {
|
|
40543
41261
|
const thisAssignRe = new RegExp(
|
|
40544
|
-
`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${
|
|
41262
|
+
`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`
|
|
40545
41263
|
);
|
|
40546
41264
|
for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
|
|
40547
41265
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|