circle-ir 4.12.0 → 4.13.1
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 +8 -0
- package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
- package/dist/analysis/constant-propagation/propagator.js +96 -3
- 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 +168 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -0
- package/dist/analysis/passes/csharp-sanitizer-credit.js +657 -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 +90 -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 +51 -54
- 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 +922 -86
- package/dist/core/circle-ir-core.cjs +89 -3
- package/dist/core/circle-ir-core.js +89 -3
- 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;
|
|
@@ -22004,7 +22021,13 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22004
22021
|
}
|
|
22005
22022
|
return;
|
|
22006
22023
|
}
|
|
22007
|
-
|
|
22024
|
+
let condValue = this.evaluateExpression(condition);
|
|
22025
|
+
if (!(isKnown(condValue) && condValue.type === "bool")) {
|
|
22026
|
+
const literal = this.literalCondition(condition);
|
|
22027
|
+
if (literal !== null) {
|
|
22028
|
+
condValue = { type: "bool", value: literal, sourceLine: getNodeLine(condition) };
|
|
22029
|
+
}
|
|
22030
|
+
}
|
|
22008
22031
|
if (isKnown(condValue) && condValue.type === "bool") {
|
|
22009
22032
|
if (condValue.value === true) {
|
|
22010
22033
|
if (alternative) {
|
|
@@ -22151,7 +22174,16 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22151
22174
|
break;
|
|
22152
22175
|
}
|
|
22153
22176
|
}
|
|
22154
|
-
|
|
22177
|
+
if (!switchValue) {
|
|
22178
|
+
const valueNode = node.childForFieldName("value");
|
|
22179
|
+
if (valueNode) {
|
|
22180
|
+
const literal = this.literalValue(valueNode);
|
|
22181
|
+
switchValue = literal !== null ? { type: typeof literal === "number" ? "int" : "string", value: literal, sourceLine: getNodeLine(valueNode) } : this.evaluateExpression(valueNode);
|
|
22182
|
+
}
|
|
22183
|
+
}
|
|
22184
|
+
const switchBlock = node.children.find(
|
|
22185
|
+
(c) => c.type === "switch_block" || c.type === "switch_body"
|
|
22186
|
+
);
|
|
22155
22187
|
if (!switchBlock) {
|
|
22156
22188
|
for (const child of node.children) {
|
|
22157
22189
|
this.visit(child);
|
|
@@ -22159,13 +22191,27 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22159
22191
|
return;
|
|
22160
22192
|
}
|
|
22161
22193
|
const caseGroups = switchBlock.children.filter(
|
|
22162
|
-
(c) => c.type === "switch_block_statement_group" || c.type === "switch_rule"
|
|
22194
|
+
(c) => c.type === "switch_block_statement_group" || c.type === "switch_rule" || c.type === "switch_section" || c.type === "switch_case" || c.type === "switch_default"
|
|
22163
22195
|
);
|
|
22196
|
+
if (caseGroups.length === 0) {
|
|
22197
|
+
this.visit(switchBlock);
|
|
22198
|
+
return;
|
|
22199
|
+
}
|
|
22164
22200
|
if (switchValue && isKnown(switchValue)) {
|
|
22165
22201
|
let matchingIdx = -1;
|
|
22166
22202
|
let defaultIdx = -1;
|
|
22167
22203
|
for (let i2 = 0; i2 < caseGroups.length; i2++) {
|
|
22168
22204
|
const caseGroup = caseGroups[i2];
|
|
22205
|
+
if (caseGroup.type === "switch_default") {
|
|
22206
|
+
defaultIdx = i2;
|
|
22207
|
+
continue;
|
|
22208
|
+
}
|
|
22209
|
+
if (caseGroup.type === "switch_case") {
|
|
22210
|
+
const label = caseGroup.childForFieldName("value");
|
|
22211
|
+
const caseValue = label ? this.literalValue(label) : null;
|
|
22212
|
+
if (caseValue !== null && caseValue === switchValue.value) matchingIdx = i2;
|
|
22213
|
+
continue;
|
|
22214
|
+
}
|
|
22169
22215
|
for (const child of caseGroup.children) {
|
|
22170
22216
|
if (child.type === "switch_label") {
|
|
22171
22217
|
const labelText = getNodeText2(child, this.source);
|
|
@@ -22177,6 +22223,16 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22177
22223
|
matchingIdx = i2;
|
|
22178
22224
|
}
|
|
22179
22225
|
}
|
|
22226
|
+
} else if (caseGroup.type === "switch_section") {
|
|
22227
|
+
if (child.type === "default") {
|
|
22228
|
+
defaultIdx = i2;
|
|
22229
|
+
} else if (child.type === "constant_pattern") {
|
|
22230
|
+
const inner = child.children.find((c) => c.isNamed);
|
|
22231
|
+
const caseValue = inner ? this.literalValue(inner) : null;
|
|
22232
|
+
if (caseValue !== null && caseValue === switchValue.value) {
|
|
22233
|
+
matchingIdx = i2;
|
|
22234
|
+
}
|
|
22235
|
+
}
|
|
22180
22236
|
}
|
|
22181
22237
|
}
|
|
22182
22238
|
}
|
|
@@ -22258,6 +22314,45 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22258
22314
|
}
|
|
22259
22315
|
return false;
|
|
22260
22316
|
}
|
|
22317
|
+
/**
|
|
22318
|
+
* A condition written with literals only, in the C# grammar's node names:
|
|
22319
|
+
* `true`, `false`, `5 == 5`, `5 != 5`. Null when it is anything else. The
|
|
22320
|
+
* shared evaluator does not know `boolean_literal` / `integer_literal`.
|
|
22321
|
+
*/
|
|
22322
|
+
literalCondition(node) {
|
|
22323
|
+
if (node.type === "parenthesized_expression") {
|
|
22324
|
+
const inner = node.children.find((c) => c.isNamed);
|
|
22325
|
+
return inner ? this.literalCondition(inner) : null;
|
|
22326
|
+
}
|
|
22327
|
+
if (node.type === "boolean_literal") {
|
|
22328
|
+
const text = getNodeText2(node, this.source);
|
|
22329
|
+
return text === "true" ? true : text === "false" ? false : null;
|
|
22330
|
+
}
|
|
22331
|
+
if (node.type === "binary_expression") {
|
|
22332
|
+
const left = node.childForFieldName("left");
|
|
22333
|
+
const right = node.childForFieldName("right");
|
|
22334
|
+
const op = node.childForFieldName("operator");
|
|
22335
|
+
const opText = op ? getNodeText2(op, this.source) : "";
|
|
22336
|
+
if (!left || !right || opText !== "==" && opText !== "!=") return null;
|
|
22337
|
+
const l = this.literalValue(left);
|
|
22338
|
+
const r = this.literalValue(right);
|
|
22339
|
+
if (l === null || r === null) return null;
|
|
22340
|
+
return opText === "==" ? l === r : l !== r;
|
|
22341
|
+
}
|
|
22342
|
+
return null;
|
|
22343
|
+
}
|
|
22344
|
+
/** The value of an integer, character or string literal node; null for anything else. */
|
|
22345
|
+
literalValue(node) {
|
|
22346
|
+
const text = getNodeText2(node, this.source);
|
|
22347
|
+
if (node.type === "integer_literal" || node.type === "decimal_integer_literal") {
|
|
22348
|
+
const n = Number(text.replace(/_/g, "").replace(/[uUlL]+$/, ""));
|
|
22349
|
+
return Number.isInteger(n) ? n : null;
|
|
22350
|
+
}
|
|
22351
|
+
if (node.type === "character_literal" || node.type === "string_literal") {
|
|
22352
|
+
return text.slice(1, -1);
|
|
22353
|
+
}
|
|
22354
|
+
return null;
|
|
22355
|
+
}
|
|
22261
22356
|
extractCaseValue(labelNode) {
|
|
22262
22357
|
for (const child of labelNode.children) {
|
|
22263
22358
|
if (child.type === "decimal_integer_literal") {
|
|
@@ -31317,7 +31412,7 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
31317
31412
|
if (!m) continue;
|
|
31318
31413
|
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
31319
31414
|
const guarded = /* @__PURE__ */ new Set([m[1]]);
|
|
31320
|
-
const
|
|
31415
|
+
const mentions2 = (t) => {
|
|
31321
31416
|
for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
|
|
31322
31417
|
return false;
|
|
31323
31418
|
};
|
|
@@ -31325,10 +31420,10 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
31325
31420
|
const lt = lines[l];
|
|
31326
31421
|
const a = assignRe.exec(lt);
|
|
31327
31422
|
if (a) {
|
|
31328
|
-
if (
|
|
31423
|
+
if (mentions2(a[2])) guarded.add(a[1]);
|
|
31329
31424
|
else if (a[1] !== m[1]) guarded.delete(a[1]);
|
|
31330
31425
|
}
|
|
31331
|
-
if (
|
|
31426
|
+
if (mentions2(lt)) {
|
|
31332
31427
|
sanitizers.push({
|
|
31333
31428
|
type: "java_canonical_startswith_guard",
|
|
31334
31429
|
method: "getCanonicalPath",
|
|
@@ -31384,14 +31479,14 @@ function findCSharpEarlyReturnGuardSanitizers(code, language) {
|
|
|
31384
31479
|
if (!a || !guarded.has(a[1])) continue;
|
|
31385
31480
|
for (const id of a[2].match(/[A-Za-z_]\w*/g) ?? []) guarded.add(id);
|
|
31386
31481
|
}
|
|
31387
|
-
const
|
|
31482
|
+
const mentions2 = (t) => {
|
|
31388
31483
|
for (const n of guarded) if (new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(t)) return true;
|
|
31389
31484
|
return false;
|
|
31390
31485
|
};
|
|
31391
31486
|
for (let l = i2 + 1; l < lines.length; l++) {
|
|
31392
31487
|
const a = assignRe.exec(lines[l]);
|
|
31393
|
-
if (a &&
|
|
31394
|
-
if (
|
|
31488
|
+
if (a && mentions2(a[2])) guarded.add(a[1]);
|
|
31489
|
+
if (mentions2(lines[l])) {
|
|
31395
31490
|
sanitizers.push({
|
|
31396
31491
|
type: "csharp_early_return_guard",
|
|
31397
31492
|
method: "guard",
|
|
@@ -31429,7 +31524,7 @@ function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
|
31429
31524
|
if (!m || !canonicalVars.has(m[1])) continue;
|
|
31430
31525
|
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
31431
31526
|
const guarded = /* @__PURE__ */ new Set([m[1]]);
|
|
31432
|
-
const
|
|
31527
|
+
const mentions2 = (t) => {
|
|
31433
31528
|
for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
|
|
31434
31529
|
return false;
|
|
31435
31530
|
};
|
|
@@ -31437,10 +31532,10 @@ function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
|
31437
31532
|
const lt = lines[l];
|
|
31438
31533
|
const a = assignRe.exec(lt);
|
|
31439
31534
|
if (a) {
|
|
31440
|
-
if (
|
|
31535
|
+
if (mentions2(a[2])) guarded.add(a[1]);
|
|
31441
31536
|
else if (a[1] !== m[1]) guarded.delete(a[1]);
|
|
31442
31537
|
}
|
|
31443
|
-
if (
|
|
31538
|
+
if (mentions2(lt)) {
|
|
31444
31539
|
sanitizers.push({
|
|
31445
31540
|
type: "csharp_fullpath_startswith_guard",
|
|
31446
31541
|
method: "GetFullPath",
|
|
@@ -35060,6 +35155,514 @@ var MyBatisAnnotationSqlSinkPass = class {
|
|
|
35060
35155
|
}
|
|
35061
35156
|
};
|
|
35062
35157
|
|
|
35158
|
+
// src/analysis/passes/csharp-sanitizer-credit.ts
|
|
35159
|
+
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*\)/;
|
|
35160
|
+
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*\)/;
|
|
35161
|
+
var CSHARP_SANITIZER_RES = [
|
|
35162
|
+
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
|
|
35163
|
+
// `HtmlEncoder.Default.Encode(x)` is the ASP.NET Core spelling (#520).
|
|
35164
|
+
{ re: /\bHtmlEncoder\s*\.\s*(?:Default\s*\.\s*)?Encode\s*\(/, type: "xss" },
|
|
35165
|
+
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
|
|
35166
|
+
// XML-escapes `< > & " '`. A value escaped this way cannot close the quoted
|
|
35167
|
+
// literal it is placed in, which is what XPath injection needs.
|
|
35168
|
+
{ re: /\bSecurityElement\s*\.\s*Escape\s*\(/, type: "xpath_injection" },
|
|
35169
|
+
// An allowlist strip leaves only inert characters, so it covers every family
|
|
35170
|
+
// whose exploitation needs a metacharacter. Each has a fixture; see
|
|
35171
|
+
// `issue-272-csharp-allowlist-strip.test.ts`.
|
|
35172
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
|
|
35173
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
|
|
35174
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
|
|
35175
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" },
|
|
35176
|
+
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "crlf" },
|
|
35177
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "ldap_injection" },
|
|
35178
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "xpath_injection" },
|
|
35179
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "command_injection" },
|
|
35180
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "sql_injection" },
|
|
35181
|
+
{ re: CSHARP_ALLOWLIST_FILTER_RE, type: "crlf" }
|
|
35182
|
+
];
|
|
35183
|
+
var INERT_WHEN_ALPHANUMERIC = /* @__PURE__ */ new Set([
|
|
35184
|
+
"ldap_injection",
|
|
35185
|
+
"xpath_injection",
|
|
35186
|
+
"command_injection",
|
|
35187
|
+
"sql_injection",
|
|
35188
|
+
"crlf",
|
|
35189
|
+
"xss",
|
|
35190
|
+
"path_traversal",
|
|
35191
|
+
"code_injection"
|
|
35192
|
+
]);
|
|
35193
|
+
var IDENT = "[A-Za-z_]\\w*";
|
|
35194
|
+
var ASSIGN_RE = new RegExp(`^\\s*(?:var\\s+|[A-Za-z_][\\w.<>\\[\\]?]*\\s+)?(${IDENT})\\s*=(?!=)\\s*(.+?);?\\s*$`);
|
|
35195
|
+
var ENDS_STATEMENT_RE = /[;{}]\s*(?:\/\/.*)?$/;
|
|
35196
|
+
function escapeRegex(s) {
|
|
35197
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
35198
|
+
}
|
|
35199
|
+
function withoutStringLiterals(text) {
|
|
35200
|
+
return text.replace(/@"(?:[^"]|"")*"|"(?:[^"\\]|\\.)*"/g, '""');
|
|
35201
|
+
}
|
|
35202
|
+
function mentions(text, name2) {
|
|
35203
|
+
return new RegExp(`(?<![\\w])${escapeRegex(name2)}(?![\\w])`).test(text);
|
|
35204
|
+
}
|
|
35205
|
+
function insideParens(text, open) {
|
|
35206
|
+
let depth = 0;
|
|
35207
|
+
for (let i2 = open; i2 < text.length; i2++) {
|
|
35208
|
+
if (text[i2] === "(") depth++;
|
|
35209
|
+
else if (text[i2] === ")" && --depth === 0) return text.slice(open + 1, i2);
|
|
35210
|
+
}
|
|
35211
|
+
return null;
|
|
35212
|
+
}
|
|
35213
|
+
function lastCallArguments(text) {
|
|
35214
|
+
const close = text.lastIndexOf(")");
|
|
35215
|
+
if (close < 0) return null;
|
|
35216
|
+
let depth = 0;
|
|
35217
|
+
for (let i2 = close; i2 >= 0; i2--) {
|
|
35218
|
+
if (text[i2] === ")") depth++;
|
|
35219
|
+
else if (text[i2] === "(" && --depth === 0) return text.slice(i2 + 1, close);
|
|
35220
|
+
}
|
|
35221
|
+
return null;
|
|
35222
|
+
}
|
|
35223
|
+
function leadingIf(text) {
|
|
35224
|
+
const m = /^\s*(?:\}\s*)?(?:else\s+)?if\s*\(/.exec(text);
|
|
35225
|
+
if (!m) return null;
|
|
35226
|
+
const open = m[0].length - 1;
|
|
35227
|
+
const cond = insideParens(text, open);
|
|
35228
|
+
if (cond === null) return null;
|
|
35229
|
+
return { cond: cond.trim(), rest: text.slice(open + cond.length + 2).trim() };
|
|
35230
|
+
}
|
|
35231
|
+
var EXITS_RE = /^(?:\{\s*)?(?:return\b|throw\b|continue\b|break\b)/;
|
|
35232
|
+
var CSharpCredit = class {
|
|
35233
|
+
constructor(code, methods) {
|
|
35234
|
+
this.methods = methods;
|
|
35235
|
+
this.lines = code.split(/\r?\n/);
|
|
35236
|
+
}
|
|
35237
|
+
methods;
|
|
35238
|
+
lines;
|
|
35239
|
+
helperCache = /* @__PURE__ */ new Map();
|
|
35240
|
+
guardHelperCache = /* @__PURE__ */ new Map();
|
|
35241
|
+
/** The innermost method containing `line`, or null. */
|
|
35242
|
+
methodAt(line) {
|
|
35243
|
+
let best = null;
|
|
35244
|
+
for (const m of this.methods) {
|
|
35245
|
+
if (line < m.start_line || line > m.end_line) continue;
|
|
35246
|
+
if (!best || m.end_line - m.start_line < best.end_line - best.start_line) best = m;
|
|
35247
|
+
}
|
|
35248
|
+
return best;
|
|
35249
|
+
}
|
|
35250
|
+
/**
|
|
35251
|
+
* Statements of lines `[from, to)`, 1-based, with a statement wrapped over
|
|
35252
|
+
* several lines joined up to its `;`. `line` is where the statement starts.
|
|
35253
|
+
*/
|
|
35254
|
+
statements(from, to) {
|
|
35255
|
+
const out2 = [];
|
|
35256
|
+
for (let i2 = Math.max(from, 1); i2 < to; i2++) {
|
|
35257
|
+
let text = (this.lines[i2 - 1] ?? "").trimEnd();
|
|
35258
|
+
const first = i2;
|
|
35259
|
+
if (/^\s*(?:[\w.<>[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !ENDS_STATEMENT_RE.test(text)) {
|
|
35260
|
+
while (i2 < to - 1 && i2 < first + 12) {
|
|
35261
|
+
i2++;
|
|
35262
|
+
text += " " + (this.lines[i2 - 1] ?? "").trim();
|
|
35263
|
+
if (ENDS_STATEMENT_RE.test(this.lines[i2 - 1] ?? "")) break;
|
|
35264
|
+
}
|
|
35265
|
+
}
|
|
35266
|
+
out2.push({ line: first, text });
|
|
35267
|
+
}
|
|
35268
|
+
return out2;
|
|
35269
|
+
}
|
|
35270
|
+
/** Does `text` apply a built-in sanitizer for `type`? */
|
|
35271
|
+
builtinSanitizes(type, text) {
|
|
35272
|
+
for (const { re, type: t } of CSHARP_SANITIZER_RES) {
|
|
35273
|
+
if (t === type && re.test(text)) return true;
|
|
35274
|
+
}
|
|
35275
|
+
if (type === "ldap_injection") {
|
|
35276
|
+
if (/\.\s*Replace\s*\(\s*"\("\s*,/.test(text) && /\.\s*Replace\s*\(\s*"\)"\s*,/.test(text)) return true;
|
|
35277
|
+
}
|
|
35278
|
+
if (type === "path_traversal") {
|
|
35279
|
+
if (/\b([A-Za-z_]\w*)\s*\.\s*StartsWith\s*\([^)]*\)\s*\?\s*\1\s*:\s*null\b/.test(text)) return true;
|
|
35280
|
+
}
|
|
35281
|
+
return false;
|
|
35282
|
+
}
|
|
35283
|
+
/**
|
|
35284
|
+
* Same-file methods that sanitize for `type`: every `return` hands back a
|
|
35285
|
+
* sanitized expression. `static string Clean(string x) { return HtmlEncode(x); }`
|
|
35286
|
+
* makes `Clean(input)` an xss sanitizer at its call sites.
|
|
35287
|
+
*/
|
|
35288
|
+
helpers(type) {
|
|
35289
|
+
const cached = this.helperCache.get(type);
|
|
35290
|
+
if (cached) return cached;
|
|
35291
|
+
const set = /* @__PURE__ */ new Set();
|
|
35292
|
+
this.helperCache.set(type, set);
|
|
35293
|
+
for (const m of this.methods) {
|
|
35294
|
+
const returns = this.returnExprs(m);
|
|
35295
|
+
if (returns.length === 0) continue;
|
|
35296
|
+
const ok = returns.every((r) => {
|
|
35297
|
+
if (this.builtinSanitizes(type, r.expr)) return true;
|
|
35298
|
+
const v = /^([A-Za-z_]\w*)$/.exec(r.expr.trim());
|
|
35299
|
+
return !!v && this.sanitizedVarsIn(type, m.start_line, r.line, false).has(v[1]);
|
|
35300
|
+
});
|
|
35301
|
+
if (ok) set.add(m.name);
|
|
35302
|
+
}
|
|
35303
|
+
return set;
|
|
35304
|
+
}
|
|
35305
|
+
/** Every returned expression of a method: `return e;` and expression bodies. */
|
|
35306
|
+
returnExprs(m) {
|
|
35307
|
+
const out2 = [];
|
|
35308
|
+
for (const st of this.statements(m.start_line, m.end_line + 1)) {
|
|
35309
|
+
for (const r of st.text.matchAll(/\breturn\s+([^;]+);/g)) out2.push({ line: st.line, expr: r[1] });
|
|
35310
|
+
if (st.line === m.start_line) {
|
|
35311
|
+
const body2 = /\)\s*=>\s*(.+?);\s*$/.exec(st.text);
|
|
35312
|
+
if (body2) out2.push({ line: st.line, expr: body2[1] });
|
|
35313
|
+
}
|
|
35314
|
+
}
|
|
35315
|
+
return out2;
|
|
35316
|
+
}
|
|
35317
|
+
/** Does `text` apply a sanitizer for `type`, built in or a same-file helper? */
|
|
35318
|
+
sanitizes(type, text) {
|
|
35319
|
+
if (this.builtinSanitizes(type, text)) return true;
|
|
35320
|
+
for (const name2 of this.helpers(type)) {
|
|
35321
|
+
if (new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`).test(text)) return true;
|
|
35322
|
+
}
|
|
35323
|
+
return false;
|
|
35324
|
+
}
|
|
35325
|
+
sanitizedVarsIn(type, from, to, withHelpers) {
|
|
35326
|
+
const sanitized = /* @__PURE__ */ new Set();
|
|
35327
|
+
for (const st of this.statements(from, to)) {
|
|
35328
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
35329
|
+
if (!m) continue;
|
|
35330
|
+
const [, lhs, rhs] = m;
|
|
35331
|
+
const credited = (withHelpers ? this.sanitizes(type, rhs) : this.builtinSanitizes(type, rhs)) || [...sanitized].some((v) => mentions(rhs, v));
|
|
35332
|
+
if (credited) sanitized.add(lhs);
|
|
35333
|
+
else sanitized.delete(lhs);
|
|
35334
|
+
}
|
|
35335
|
+
return sanitized;
|
|
35336
|
+
}
|
|
35337
|
+
/**
|
|
35338
|
+
* The variables sanitized for `type` at `sinkLine`, judged inside the method
|
|
35339
|
+
* that contains the sink (cognium-dev#579). The body is read top to bottom up
|
|
35340
|
+
* to the sink and the last assignment to a name decides: a sanitizer call or
|
|
35341
|
+
* a derivation from a sanitized name credits it, anything else removes it.
|
|
35342
|
+
* Class fields are the caller's concern: they can be sanitized in one method
|
|
35343
|
+
* and read in another.
|
|
35344
|
+
*/
|
|
35345
|
+
sanitizedVarsAt(type, sinkLine) {
|
|
35346
|
+
return this.sanitizedVarsIn(type, this.methodAt(sinkLine)?.start_line ?? 1, sinkLine, true);
|
|
35347
|
+
}
|
|
35348
|
+
/**
|
|
35349
|
+
* True when the sink on `sinkLine` also receives a tainted value that no
|
|
35350
|
+
* sanitizer covers. In `Write(HtmlEncode(a) + b)` the encoder covers `a`
|
|
35351
|
+
* only: `b` arrives raw, so the sanitizer on the line must not clear the
|
|
35352
|
+
* sink. `credited` are the names already known safe for `type` there;
|
|
35353
|
+
* `seeds` are the taint sources that name a variable.
|
|
35354
|
+
*
|
|
35355
|
+
* Taint is followed through the sink's method top to bottom, last
|
|
35356
|
+
* assignment wins. The sink text is then read without its string literals
|
|
35357
|
+
* and without the sanitizer calls for `type`; a tainted, uncredited name
|
|
35358
|
+
* left over is a raw operand.
|
|
35359
|
+
*/
|
|
35360
|
+
rawTaintReaches(type, sinkLine, credited, seeds) {
|
|
35361
|
+
if (seeds.length === 0) return false;
|
|
35362
|
+
const method = this.methodAt(sinkLine);
|
|
35363
|
+
const from = method?.start_line ?? 1;
|
|
35364
|
+
const tainted = /* @__PURE__ */ new Set();
|
|
35365
|
+
const stmts = this.statements(from, sinkLine);
|
|
35366
|
+
for (let i2 = 0; i2 < stmts.length; i2++) {
|
|
35367
|
+
const st = stmts[i2];
|
|
35368
|
+
const end = i2 + 1 < stmts.length ? stmts[i2 + 1].line : sinkLine;
|
|
35369
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
35370
|
+
if (m) {
|
|
35371
|
+
const rhs = withoutStringLiterals(m[2]);
|
|
35372
|
+
if ([...tainted].some((v) => mentions(rhs, v))) tainted.add(m[1]);
|
|
35373
|
+
else tainted.delete(m[1]);
|
|
35374
|
+
}
|
|
35375
|
+
for (const s of seeds) if (s.line >= st.line && s.line < end) tainted.add(s.variable);
|
|
35376
|
+
}
|
|
35377
|
+
if (tainted.size === 0) return false;
|
|
35378
|
+
const call = withoutStringLiterals(this.sinkText(sinkLine));
|
|
35379
|
+
const rest = this.withoutSanitizerCalls(type, lastCallArguments(call) ?? call);
|
|
35380
|
+
if (this.builtinSanitizes(type, rest)) return false;
|
|
35381
|
+
for (const v of tainted) if (!credited.has(v) && mentions(rest, v)) return true;
|
|
35382
|
+
return false;
|
|
35383
|
+
}
|
|
35384
|
+
/** `text` with every sanitizer call for `type`, arguments included, removed. */
|
|
35385
|
+
withoutSanitizerCalls(type, text) {
|
|
35386
|
+
const res = [];
|
|
35387
|
+
for (const { re, type: t } of CSHARP_SANITIZER_RES) if (t === type) res.push(re);
|
|
35388
|
+
for (const name2 of this.helpers(type)) res.push(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`));
|
|
35389
|
+
let out2 = text;
|
|
35390
|
+
for (const re of res) {
|
|
35391
|
+
for (let guard = 0; guard < 16; guard++) {
|
|
35392
|
+
const m = re.exec(out2);
|
|
35393
|
+
if (!m) break;
|
|
35394
|
+
if (!m[0].endsWith("(")) {
|
|
35395
|
+
out2 = out2.slice(0, m.index) + out2.slice(m.index + m[0].length);
|
|
35396
|
+
continue;
|
|
35397
|
+
}
|
|
35398
|
+
const open = m.index + m[0].length - 1;
|
|
35399
|
+
const inner = insideParens(out2, open);
|
|
35400
|
+
if (inner === null) break;
|
|
35401
|
+
out2 = out2.slice(0, m.index) + out2.slice(open + inner.length + 2);
|
|
35402
|
+
}
|
|
35403
|
+
}
|
|
35404
|
+
return out2;
|
|
35405
|
+
}
|
|
35406
|
+
/**
|
|
35407
|
+
* The text of the statement at `line` that a sink call can be in: for
|
|
35408
|
+
* `if (COND) sink(x);` that is `sink(x);`, without the condition. A name
|
|
35409
|
+
* that only appears in the condition is not something the sink receives.
|
|
35410
|
+
*/
|
|
35411
|
+
sinkText(line) {
|
|
35412
|
+
const text = this.lines[line - 1] ?? "";
|
|
35413
|
+
const guarded = leadingIf(text);
|
|
35414
|
+
return guarded && guarded.rest !== "" ? guarded.rest : text;
|
|
35415
|
+
}
|
|
35416
|
+
// -------------------------------------------------------------------------
|
|
35417
|
+
// Guards
|
|
35418
|
+
// -------------------------------------------------------------------------
|
|
35419
|
+
/** Variables a condition proves to hold only letters, digits, `_` and `-`. */
|
|
35420
|
+
allowlistVars(cond) {
|
|
35421
|
+
const out2 = [];
|
|
35422
|
+
const anchored = new RegExp(
|
|
35423
|
+
`\\bRegex\\s*\\.\\s*IsMatch\\s*\\(\\s*(${IDENT})\\s*,\\s*@?"\\^\\[(?:[A-Za-z0-9_\\- ]|\\\\w|\\\\d)+\\](?:[+*]|\\{\\d+(?:,\\d*)?\\})\\$"`,
|
|
35424
|
+
"g"
|
|
35425
|
+
);
|
|
35426
|
+
for (const m of cond.matchAll(anchored)) out2.push(m[1]);
|
|
35427
|
+
const methodGroup = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*char\\s*\\.\\s*Is(?:LetterOrDigit|Digit|Letter)\\s*\\)`, "g");
|
|
35428
|
+
for (const m of cond.matchAll(methodGroup)) out2.push(m[1]);
|
|
35429
|
+
const lambda = new RegExp(`(${IDENT})\\s*\\.\\s*All\\s*\\(\\s*(${IDENT})\\s*=>`, "g");
|
|
35430
|
+
for (const m of cond.matchAll(lambda)) {
|
|
35431
|
+
const open = cond.indexOf("(", m.index + m[1].length);
|
|
35432
|
+
const body2 = insideParens(cond, open)?.replace(new RegExp(`^\\s*${m[2]}\\s*=>`), "") ?? "";
|
|
35433
|
+
const c = escapeRegex(m[2]);
|
|
35434
|
+
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, "");
|
|
35435
|
+
if (residue === "") out2.push(m[1]);
|
|
35436
|
+
}
|
|
35437
|
+
return out2;
|
|
35438
|
+
}
|
|
35439
|
+
/** Variables a condition proves to be numeric (`int.TryParse(x, out _)`). */
|
|
35440
|
+
numericVars(cond, parsedFrom) {
|
|
35441
|
+
const out2 = [];
|
|
35442
|
+
const tryParse = new RegExp(
|
|
35443
|
+
`\\b(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64|Guid)\\s*\\.\\s*TryParse\\s*\\(\\s*(${IDENT})\\s*,`,
|
|
35444
|
+
"g"
|
|
35445
|
+
);
|
|
35446
|
+
for (const m of cond.matchAll(tryParse)) out2.push(m[1]);
|
|
35447
|
+
for (const [parsed, from] of parsedFrom) {
|
|
35448
|
+
if (new RegExp(`(?<![\\w])${escapeRegex(parsed)}\\s*(?:!=\\s*null|\\.\\s*HasValue)\\b`).test(cond)) out2.push(from);
|
|
35449
|
+
}
|
|
35450
|
+
return out2;
|
|
35451
|
+
}
|
|
35452
|
+
/** Variables whose host a condition compares against a constant or an allowlist. */
|
|
35453
|
+
hostVars(cond, hostOf) {
|
|
35454
|
+
const out2 = [];
|
|
35455
|
+
const compares = (expr) => {
|
|
35456
|
+
const e = escapeRegex(expr).replace(/\\ /g, "\\s*");
|
|
35457
|
+
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);
|
|
35458
|
+
};
|
|
35459
|
+
const direct = new RegExp(`new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host\\b`, "g");
|
|
35460
|
+
for (const m of cond.matchAll(direct)) {
|
|
35461
|
+
if (compares(m[0])) out2.push(m[1]);
|
|
35462
|
+
}
|
|
35463
|
+
for (const [hostVar, from] of hostOf) {
|
|
35464
|
+
if (mentions(cond, hostVar) && compares(hostVar)) out2.push(from);
|
|
35465
|
+
}
|
|
35466
|
+
return out2;
|
|
35467
|
+
}
|
|
35468
|
+
/**
|
|
35469
|
+
* Same-file methods that are guards of `kind` on one of their parameters:
|
|
35470
|
+
* `static bool IsAllowed(string c) { return Array.IndexOf(Allowed, new Uri(c).Host) >= 0; }`.
|
|
35471
|
+
* Maps the method name to the position of the validated parameter.
|
|
35472
|
+
*/
|
|
35473
|
+
guardHelpers(kind) {
|
|
35474
|
+
const cached = this.guardHelperCache.get(kind);
|
|
35475
|
+
if (cached) return cached;
|
|
35476
|
+
const map = /* @__PURE__ */ new Map();
|
|
35477
|
+
this.guardHelperCache.set(kind, map);
|
|
35478
|
+
for (const m of this.methods) {
|
|
35479
|
+
const signature = this.lines[m.start_line - 1] ?? "";
|
|
35480
|
+
const open = signature.indexOf("(", signature.indexOf(m.name));
|
|
35481
|
+
const params = open === -1 ? null : insideParens(signature, open);
|
|
35482
|
+
if (!params) continue;
|
|
35483
|
+
const names = params.split(",").map((p) => p.trim().split(/\s+/).pop() ?? "");
|
|
35484
|
+
const returns = this.returnExprs(m);
|
|
35485
|
+
if (returns.length !== 1) continue;
|
|
35486
|
+
const proven = this.provenBy(kind, returns[0].expr, /* @__PURE__ */ new Map(), /* @__PURE__ */ new Map(), false);
|
|
35487
|
+
const pos = names.findIndex((n) => proven.includes(n));
|
|
35488
|
+
if (pos !== -1) map.set(m.name, pos);
|
|
35489
|
+
}
|
|
35490
|
+
return map;
|
|
35491
|
+
}
|
|
35492
|
+
provenBy(kind, cond, parsedFrom, hostOf, withHelpers) {
|
|
35493
|
+
const out2 = kind === "allowlist" ? this.allowlistVars(cond) : kind === "numeric" ? this.numericVars(cond, parsedFrom) : this.hostVars(cond, hostOf);
|
|
35494
|
+
if (withHelpers) {
|
|
35495
|
+
for (const [name2, pos] of this.guardHelpers(kind)) {
|
|
35496
|
+
for (const call of cond.matchAll(new RegExp(`(?<![\\w.])${escapeRegex(name2)}\\s*\\(`, "g"))) {
|
|
35497
|
+
const args2 = insideParens(cond, call.index + call[0].length - 1)?.split(",") ?? [];
|
|
35498
|
+
const arg = args2[pos]?.trim();
|
|
35499
|
+
if (arg && new RegExp(`^${IDENT}$`).test(arg)) out2.push(arg);
|
|
35500
|
+
}
|
|
35501
|
+
}
|
|
35502
|
+
}
|
|
35503
|
+
return out2;
|
|
35504
|
+
}
|
|
35505
|
+
/**
|
|
35506
|
+
* The variables a check dominating `sinkLine` proves safe for `type`.
|
|
35507
|
+
*
|
|
35508
|
+
* Recognised checks:
|
|
35509
|
+
* - allowlist: anchored `Regex.IsMatch(x, "^[A-Za-z0-9_]+$")`, `x.All(char.IsLetterOrDigit)`;
|
|
35510
|
+
* - numeric: `int.TryParse(x, out _)`, or `p != null` where `p = int.Parse(x)`;
|
|
35511
|
+
* - host (ssrf only): `new Uri(x).Host == "api.example.com"`, an allowlist
|
|
35512
|
+
* `Contains` / `IndexOf` of that host.
|
|
35513
|
+
* directly, through a `bool` or host variable, or through a same-file helper.
|
|
35514
|
+
*
|
|
35515
|
+
* A check dominates the sink when the sink is the body of the `if`, is
|
|
35516
|
+
* inside its block, or follows an early exit on the negated check
|
|
35517
|
+
* (`if (!ok) return;`).
|
|
35518
|
+
*/
|
|
35519
|
+
guardedVarsAt(type, sinkLine) {
|
|
35520
|
+
const kinds = [];
|
|
35521
|
+
if (INERT_WHEN_ALPHANUMERIC.has(type)) kinds.push("allowlist", "numeric");
|
|
35522
|
+
if (type === "ssrf") kinds.push("host");
|
|
35523
|
+
const proven = /* @__PURE__ */ new Set();
|
|
35524
|
+
if (kinds.length === 0) return proven;
|
|
35525
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
35526
|
+
const before = this.statements(scopeStart, sinkLine);
|
|
35527
|
+
const boolOf = /* @__PURE__ */ new Map();
|
|
35528
|
+
const parsedFrom = /* @__PURE__ */ new Map();
|
|
35529
|
+
const hostOf = /* @__PURE__ */ new Map();
|
|
35530
|
+
const parseAnywhere = new RegExp(
|
|
35531
|
+
`(${IDENT})\\s*=\\s*(?:\\([^)]*\\)\\s*)?(?:int|long|short|byte|uint|ulong|decimal|double|float|Int32|Int64)\\s*\\.\\s*Parse\\s*\\(\\s*(${IDENT})\\s*[,)]`,
|
|
35532
|
+
"g"
|
|
35533
|
+
);
|
|
35534
|
+
for (const st of before) {
|
|
35535
|
+
for (const p of st.text.matchAll(parseAnywhere)) parsedFrom.set(p[1], p[2]);
|
|
35536
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
35537
|
+
if (!m) continue;
|
|
35538
|
+
const [, lhs, rhs] = m;
|
|
35539
|
+
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);
|
|
35540
|
+
if (parse2) parsedFrom.set(lhs, parse2[1]);
|
|
35541
|
+
const host = new RegExp(`^new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*(${IDENT})\\s*\\)\\s*\\.\\s*Host$`).exec(rhs.trim());
|
|
35542
|
+
if (host) hostOf.set(lhs, host[1]);
|
|
35543
|
+
boolOf.set(lhs, rhs);
|
|
35544
|
+
}
|
|
35545
|
+
const credit = (cond) => {
|
|
35546
|
+
const resolved = new RegExp(`^${IDENT}$`).test(cond) ? boolOf.get(cond) ?? cond : cond;
|
|
35547
|
+
for (const kind of kinds) {
|
|
35548
|
+
for (const v of this.provenBy(kind, resolved, parsedFrom, hostOf, true)) proven.add(v);
|
|
35549
|
+
}
|
|
35550
|
+
};
|
|
35551
|
+
const own = leadingIf(this.lines[sinkLine - 1] ?? "");
|
|
35552
|
+
if (own && !own.cond.startsWith("!")) credit(own.cond);
|
|
35553
|
+
let depth = 0;
|
|
35554
|
+
for (let i2 = sinkLine - 1; i2 >= scopeStart; i2--) {
|
|
35555
|
+
const text = this.lines[i2 - 1] ?? "";
|
|
35556
|
+
for (let c = text.length - 1; c >= 0; c--) {
|
|
35557
|
+
if (text[c] === "}") depth++;
|
|
35558
|
+
else if (text[c] === "{") {
|
|
35559
|
+
if (depth > 0) {
|
|
35560
|
+
depth--;
|
|
35561
|
+
continue;
|
|
35562
|
+
}
|
|
35563
|
+
let header = text.slice(0, c);
|
|
35564
|
+
for (let h = i2 - 1; header.trim() === "" && h >= scopeStart; h--) header = this.lines[h - 1] ?? "";
|
|
35565
|
+
const enclosing = leadingIf(header);
|
|
35566
|
+
if (enclosing && !enclosing.cond.startsWith("!")) credit(enclosing.cond);
|
|
35567
|
+
}
|
|
35568
|
+
}
|
|
35569
|
+
}
|
|
35570
|
+
for (let i2 = scopeStart; i2 < sinkLine; i2++) {
|
|
35571
|
+
const guard = leadingIf(this.lines[i2 - 1] ?? "");
|
|
35572
|
+
if (!guard) continue;
|
|
35573
|
+
let rest = guard.rest;
|
|
35574
|
+
for (let n = i2 + 1; rest === "" && n < sinkLine; n++) rest = (this.lines[n - 1] ?? "").trim();
|
|
35575
|
+
if (!EXITS_RE.test(rest)) continue;
|
|
35576
|
+
if (guard.cond.startsWith("!")) {
|
|
35577
|
+
const inner = guard.cond.slice(1).trim();
|
|
35578
|
+
const wrapped = inner.startsWith("(") && insideParens(inner, 0)?.length === inner.length - 2;
|
|
35579
|
+
credit(wrapped ? inner.slice(1, -1) : inner);
|
|
35580
|
+
} else if (/!=/.test(guard.cond) && !/[|&]/.test(guard.cond)) credit(guard.cond.replace("!=", "=="));
|
|
35581
|
+
}
|
|
35582
|
+
return proven;
|
|
35583
|
+
}
|
|
35584
|
+
/**
|
|
35585
|
+
* `StringBuilder`s that hold only safe text at `sinkLine`: every write to
|
|
35586
|
+
* the builder before the sink is made of string literals and `credited`
|
|
35587
|
+
* names. One write with anything else and the builder is not credited, so
|
|
35588
|
+
* `sb.Append(validated); sb.Append(raw);` stays tainted.
|
|
35589
|
+
*
|
|
35590
|
+
* This is how a guard on a value reaches a sink that receives the builder
|
|
35591
|
+
* the value was appended to (`if (parsed != null) { sb.Append(data); Compile(sb.ToString()); }`).
|
|
35592
|
+
*/
|
|
35593
|
+
safeBuilders(credited, sinkLine) {
|
|
35594
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
35595
|
+
const onlySafe = (expr) => {
|
|
35596
|
+
const idents = expr.replace(/\$?@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ").replace(/\.\s*[A-Za-z_]\w*\s*\(\s*\)/g, " ").match(/[A-Za-z_]\w*/g) ?? [];
|
|
35597
|
+
return idents.every((id) => credited.has(id));
|
|
35598
|
+
};
|
|
35599
|
+
const state = /* @__PURE__ */ new Map();
|
|
35600
|
+
const declRe = new RegExp(`\\b(?:StringBuilder|var)\\s+(${IDENT})\\s*=\\s*new\\s+(?:System\\s*\\.\\s*Text\\s*\\.\\s*)?StringBuilder\\s*\\(`);
|
|
35601
|
+
const writeRe = new RegExp(`^\\s*(${IDENT})\\s*\\.\\s*(?:Append|AppendLine|AppendFormat|Insert)\\s*\\(`);
|
|
35602
|
+
for (const st of this.statements(scopeStart, sinkLine)) {
|
|
35603
|
+
const decl = declRe.exec(st.text);
|
|
35604
|
+
if (decl) {
|
|
35605
|
+
const args2 = insideParens(st.text, decl.index + decl[0].length - 1) ?? "";
|
|
35606
|
+
state.set(decl[1], onlySafe(args2));
|
|
35607
|
+
continue;
|
|
35608
|
+
}
|
|
35609
|
+
const write = writeRe.exec(st.text);
|
|
35610
|
+
if (write && state.has(write[1])) {
|
|
35611
|
+
const args2 = insideParens(st.text, write[0].length - 1);
|
|
35612
|
+
if (args2 === null || !onlySafe(args2)) state.set(write[1], false);
|
|
35613
|
+
}
|
|
35614
|
+
}
|
|
35615
|
+
return new Set([...state].filter(([, safe]) => safe).map(([name2]) => name2));
|
|
35616
|
+
}
|
|
35617
|
+
/**
|
|
35618
|
+
* True when what leaves on `line` has been sanitized or validated for some
|
|
35619
|
+
* sink type. Used for the untyped "tainted data reaches an external call"
|
|
35620
|
+
* fallback, which has no sink type of its own to ask about.
|
|
35621
|
+
*/
|
|
35622
|
+
creditedOnLine(line, seeds = []) {
|
|
35623
|
+
const text = this.sinkText(line);
|
|
35624
|
+
const types = new Set(CSHARP_SANITIZER_RES.map((r) => r.type));
|
|
35625
|
+
for (const t of INERT_WHEN_ALPHANUMERIC) types.add(t);
|
|
35626
|
+
types.add("ssrf");
|
|
35627
|
+
for (const type of types) {
|
|
35628
|
+
const credited = /* @__PURE__ */ new Set([...this.sanitizedVarsAt(type, line), ...this.guardedVarsAt(type, line)]);
|
|
35629
|
+
for (const v of this.safeBuilders(credited, line)) credited.add(v);
|
|
35630
|
+
const covered = this.sanitizes(type, text) || [...credited].some((v) => mentions(text, v));
|
|
35631
|
+
if (covered && !this.rawTaintReaches(type, line, credited, seeds)) return true;
|
|
35632
|
+
}
|
|
35633
|
+
return this.fixedHostRequest(line);
|
|
35634
|
+
}
|
|
35635
|
+
// -------------------------------------------------------------------------
|
|
35636
|
+
// SSRF: constant host
|
|
35637
|
+
// -------------------------------------------------------------------------
|
|
35638
|
+
/**
|
|
35639
|
+
* True when the request on `sinkLine` goes to a constant host: the URL
|
|
35640
|
+
* argument starts with a string literal holding the whole authority, as in
|
|
35641
|
+
* `GetAsync("https://api.internal.example.com/?x=" + v)`. Input can then only
|
|
35642
|
+
* extend the path or query. The literal must be the first operand of the
|
|
35643
|
+
* argument, and must end the authority (`/`, `?` or `#` after the host), or
|
|
35644
|
+
* `"https://api.example.com" + x` would let `x = ".evil.org"` move the host.
|
|
35645
|
+
*/
|
|
35646
|
+
fixedHostRequest(sinkLine) {
|
|
35647
|
+
const fixedLiteral = '\\$?@?"https?:\\/\\/[A-Za-z0-9.\\-]+(?::\\d+)?[\\/?#][^"]*"';
|
|
35648
|
+
const text = this.lines[sinkLine - 1] ?? "";
|
|
35649
|
+
const argStart = `\\(\\s*(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?`;
|
|
35650
|
+
if (new RegExp(argStart + fixedLiteral).test(text)) return true;
|
|
35651
|
+
const scopeStart = this.methodAt(sinkLine)?.start_line ?? 1;
|
|
35652
|
+
const fixed = /* @__PURE__ */ new Set();
|
|
35653
|
+
for (const st of this.statements(scopeStart, sinkLine)) {
|
|
35654
|
+
const m = ASSIGN_RE.exec(st.text);
|
|
35655
|
+
if (!m) continue;
|
|
35656
|
+
if (new RegExp("^(?:new\\s+(?:System\\s*\\.\\s*)?Uri\\s*\\(\\s*)?" + fixedLiteral).test(m[2].trim())) fixed.add(m[1]);
|
|
35657
|
+
else fixed.delete(m[1]);
|
|
35658
|
+
}
|
|
35659
|
+
for (const v of fixed) {
|
|
35660
|
+
if (new RegExp(argStart + escapeRegex(v) + "\\s*[,)]").test(text)) return true;
|
|
35661
|
+
}
|
|
35662
|
+
return false;
|
|
35663
|
+
}
|
|
35664
|
+
};
|
|
35665
|
+
|
|
35063
35666
|
// src/analysis/passes/sink-filter-pass.ts
|
|
35064
35667
|
function escapeReSf(s) {
|
|
35065
35668
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -35350,7 +35953,6 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
35350
35953
|
}
|
|
35351
35954
|
return covered;
|
|
35352
35955
|
}
|
|
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
35956
|
function isUnwrittenLocalMapKeyRead(code, source) {
|
|
35355
35957
|
if (source.type !== "plugin_param") return false;
|
|
35356
35958
|
const lines = code.split("\n");
|
|
@@ -35372,18 +35974,6 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
35372
35974
|
if (lines.some((l) => putRe.test(l))) return false;
|
|
35373
35975
|
return true;
|
|
35374
35976
|
}
|
|
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
35977
|
function csharpSanitizedVarsByType(code) {
|
|
35388
35978
|
const lines = code.split("\n");
|
|
35389
35979
|
const byType = /* @__PURE__ */ new Map();
|
|
@@ -35411,7 +36001,7 @@ function csharpSanitizedVarsByType(code) {
|
|
|
35411
36001
|
for (const [type, set] of byType) {
|
|
35412
36002
|
if (set.has(lhs)) continue;
|
|
35413
36003
|
const refsSanitized = [...set].some(
|
|
35414
|
-
(v) => new RegExp(`(?<![\\w])${
|
|
36004
|
+
(v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(rhs)
|
|
35415
36005
|
);
|
|
35416
36006
|
if (refsSanitized) {
|
|
35417
36007
|
set.add(lhs);
|
|
@@ -36202,26 +36792,32 @@ var SinkFilterPass = class {
|
|
|
36202
36792
|
if (language === "csharp") {
|
|
36203
36793
|
const sourceLines = ctx.code.split("\n");
|
|
36204
36794
|
const sanitizedByType = csharpSanitizedVarsByType(ctx.code);
|
|
36795
|
+
const csFields = new Set(ctx.graph.ir.types.flatMap((t) => (t.fields ?? []).map((f) => f.name)));
|
|
36796
|
+
const credit = new CSharpCredit(ctx.code, ctx.graph.ir.types.flatMap((t) => t.methods));
|
|
36797
|
+
const csSeeds = mergedSources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
|
|
36205
36798
|
const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
|
|
36206
36799
|
filtered = filtered.filter((sink) => {
|
|
36207
36800
|
if (sink.type === "xxe" && xxeHardened) return false;
|
|
36208
36801
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
36209
|
-
|
|
36210
|
-
|
|
36211
|
-
|
|
36212
|
-
const
|
|
36213
|
-
|
|
36214
|
-
|
|
36215
|
-
|
|
36216
|
-
|
|
36217
|
-
|
|
36218
|
-
|
|
36802
|
+
const inlineSanitized = credit.sanitizes(sink.type, sinkLineText);
|
|
36803
|
+
if (sink.type === "ssrf" && credit.fixedHostRequest(sink.line)) return false;
|
|
36804
|
+
const scoped = credit.sanitizedVarsAt(sink.type, sink.line);
|
|
36805
|
+
const fileWide = sanitizedByType.get(sink.type);
|
|
36806
|
+
const sanitizedVars = new Set(scoped);
|
|
36807
|
+
if (fileWide) {
|
|
36808
|
+
for (const v of fileWide) if (csFields.has(v)) sanitizedVars.add(v);
|
|
36809
|
+
}
|
|
36810
|
+
for (const v of credit.guardedVarsAt(sink.type, sink.line)) sanitizedVars.add(v);
|
|
36811
|
+
for (const v of credit.safeBuilders(sanitizedVars, sink.line)) sanitizedVars.add(v);
|
|
36812
|
+
const received = credit.sinkText(sink.line);
|
|
36813
|
+
const covered = inlineSanitized || [...sanitizedVars].some((v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(received));
|
|
36814
|
+
return !covered || credit.rawTaintReaches(sink.type, sink.line, sanitizedVars, csSeeds);
|
|
36219
36815
|
});
|
|
36220
36816
|
}
|
|
36221
36817
|
if (language === "csharp") {
|
|
36222
36818
|
const sourceLines = ctx.code.split("\n");
|
|
36223
36819
|
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*\)/;
|
|
36820
|
+
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
36821
|
for (const line of sourceLines) {
|
|
36226
36822
|
const m = declRe.exec(line);
|
|
36227
36823
|
if (m) constBaseClients.add(m[1]);
|
|
@@ -36233,13 +36829,20 @@ var SinkFilterPass = class {
|
|
|
36233
36829
|
const text = sourceLines[sink.line - 1] ?? "";
|
|
36234
36830
|
let onConstClient = false;
|
|
36235
36831
|
for (const v of constBaseClients) {
|
|
36236
|
-
if (new RegExp(`(?<![\\w])${
|
|
36832
|
+
if (new RegExp(`(?<![\\w])${escapeRegex2(v)}\\s*\\.`).test(text)) {
|
|
36237
36833
|
onConstClient = true;
|
|
36238
36834
|
break;
|
|
36239
36835
|
}
|
|
36240
36836
|
}
|
|
36241
36837
|
if (!onConstClient) return true;
|
|
36242
|
-
|
|
36838
|
+
let m = relativeLiteralArgRe.exec(text);
|
|
36839
|
+
if (!m) {
|
|
36840
|
+
const arg = /Async\s*\(\s*([A-Za-z_]\w*)\s*[,)]/.exec(text)?.[1];
|
|
36841
|
+
if (arg) {
|
|
36842
|
+
const assigned = new RegExp(`\\b${escapeRegex2(arg)}\\s*=\\s*(?:@?\\$?)"([^"]*)"`);
|
|
36843
|
+
for (let i2 = sink.line - 2; i2 >= 0 && !m; i2--) m = assigned.exec(sourceLines[i2] ?? "");
|
|
36844
|
+
}
|
|
36845
|
+
}
|
|
36243
36846
|
if (!m) return true;
|
|
36244
36847
|
const lit = m[1];
|
|
36245
36848
|
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(lit)) return true;
|
|
@@ -36269,7 +36872,7 @@ var SinkFilterPass = class {
|
|
|
36269
36872
|
const allLdapSanitizers = [...PY_BUILTIN_LDAP_SANITIZERS, ...wrappers];
|
|
36270
36873
|
if (allLdapSanitizers.length > 0) {
|
|
36271
36874
|
const ldapPatterns = allLdapSanitizers.map(
|
|
36272
|
-
(name2) => new RegExp(`\\b${
|
|
36875
|
+
(name2) => new RegExp(`\\b${escapeRegex2(name2)}\\s*\\(`)
|
|
36273
36876
|
);
|
|
36274
36877
|
filtered = filtered.filter((sink) => {
|
|
36275
36878
|
if (sink.type !== "ldap_injection") return true;
|
|
@@ -36341,7 +36944,7 @@ var SinkFilterPass = class {
|
|
|
36341
36944
|
return { sources, sinks: filtered, sanitizers };
|
|
36342
36945
|
}
|
|
36343
36946
|
};
|
|
36344
|
-
function
|
|
36947
|
+
function escapeRegex2(s) {
|
|
36345
36948
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36346
36949
|
}
|
|
36347
36950
|
var RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
@@ -36425,7 +37028,7 @@ function extractSourceIdentifiers(text) {
|
|
|
36425
37028
|
return out2;
|
|
36426
37029
|
}
|
|
36427
37030
|
function isAssignedFromSanitizerPattern(sourceLines, sinkLine, varName, sanitizerPatterns, lookback = 30) {
|
|
36428
|
-
const escapedVar =
|
|
37031
|
+
const escapedVar = escapeRegex2(varName);
|
|
36429
37032
|
const assignRe = new RegExp(
|
|
36430
37033
|
`(?:\\b(?:const|let|var|final|String)\\s+)?\\b${escapedVar}\\s*=\\s*(.+)`
|
|
36431
37034
|
);
|
|
@@ -37895,7 +38498,7 @@ var LibraryProfileXssGatePass = class {
|
|
|
37895
38498
|
|
|
37896
38499
|
// src/analysis/passes/taint-propagation-pass.ts
|
|
37897
38500
|
var CONTAINER_READ_SOURCES = /* @__PURE__ */ new Set(["plugin_param", "config_param"]);
|
|
37898
|
-
function makeConstPropVeto(constProp, graph, sources, code) {
|
|
38501
|
+
function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
37899
38502
|
const lines = typeof code === "string" ? code.split("\n") : [];
|
|
37900
38503
|
const selfReassign = /* @__PURE__ */ new Map();
|
|
37901
38504
|
const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -37925,6 +38528,7 @@ function makeConstPropVeto(constProp, graph, sources, code) {
|
|
|
37925
38528
|
const r = isFalsePositive(constProp, step.line, step.variable);
|
|
37926
38529
|
if (!r.isFalsePositive) return false;
|
|
37927
38530
|
if (r.reason !== "variable_not_tainted") return true;
|
|
38531
|
+
if (language === "csharp") return false;
|
|
37928
38532
|
const lo = Math.min(sourceLine, sinkLine);
|
|
37929
38533
|
let d = defAt(step.variable, step.line);
|
|
37930
38534
|
let selfHops = 0;
|
|
@@ -37958,7 +38562,7 @@ var TaintPropagationPass = class {
|
|
|
37958
38562
|
return { flows: [] };
|
|
37959
38563
|
}
|
|
37960
38564
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
37961
|
-
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
38565
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
|
|
37962
38566
|
const verifiedFlows = propagationResult.flows.filter((flow) => {
|
|
37963
38567
|
if (constProp.unreachableLines.has(flow.sink.line)) return false;
|
|
37964
38568
|
for (const step of flow.path) {
|
|
@@ -38457,7 +39061,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
38457
39061
|
if (source.variable && source.variable !== varName && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
38458
39062
|
continue;
|
|
38459
39063
|
}
|
|
38460
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
|
|
39064
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line, unreachableLines)) {
|
|
38461
39065
|
continue;
|
|
38462
39066
|
}
|
|
38463
39067
|
flows.push({
|
|
@@ -38497,7 +39101,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
38497
39101
|
if (source.variable && source.variable !== collectionVar && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
38498
39102
|
continue;
|
|
38499
39103
|
}
|
|
38500
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
|
|
39104
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line, unreachableLines)) {
|
|
38501
39105
|
continue;
|
|
38502
39106
|
}
|
|
38503
39107
|
flows.push({
|
|
@@ -38611,7 +39215,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
38611
39215
|
if (paramSource) {
|
|
38612
39216
|
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
38613
39217
|
if (!exists) {
|
|
38614
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
|
|
39218
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line, unreachableLines)) {
|
|
38615
39219
|
continue;
|
|
38616
39220
|
}
|
|
38617
39221
|
flows.push({
|
|
@@ -38635,6 +39239,85 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
38635
39239
|
void types;
|
|
38636
39240
|
return flows;
|
|
38637
39241
|
}
|
|
39242
|
+
function csharpMethodScopedAliases(code, seeds, methods, fieldNames) {
|
|
39243
|
+
const lines = code.split("\n");
|
|
39244
|
+
const methodOf = (line) => {
|
|
39245
|
+
let best = -1;
|
|
39246
|
+
let span = Infinity;
|
|
39247
|
+
methods.forEach((m, i2) => {
|
|
39248
|
+
if (line >= m.start_line && line <= m.end_line && m.end_line - m.start_line < span) {
|
|
39249
|
+
best = i2;
|
|
39250
|
+
span = m.end_line - m.start_line;
|
|
39251
|
+
}
|
|
39252
|
+
});
|
|
39253
|
+
return best;
|
|
39254
|
+
};
|
|
39255
|
+
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*$/;
|
|
39256
|
+
const assignRe = /^\s*(?:this\s*\.\s*)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
39257
|
+
const returnRe = /^\s*return\s+(.+?);\s*$/;
|
|
39258
|
+
const KEYWORDS2 = /* @__PURE__ */ new Set(["return", "throw", "new", "await", "yield", "else", "case", "using", "var"]);
|
|
39259
|
+
const codeOf = (rhs) => rhs.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
|
|
39260
|
+
const local = /* @__PURE__ */ new Map();
|
|
39261
|
+
const taintedFields = /* @__PURE__ */ new Set();
|
|
39262
|
+
const returnsTainted = /* @__PURE__ */ new Set();
|
|
39263
|
+
const localsOf = (m) => {
|
|
39264
|
+
let set = local.get(m);
|
|
39265
|
+
if (!set) {
|
|
39266
|
+
set = /* @__PURE__ */ new Set();
|
|
39267
|
+
local.set(m, set);
|
|
39268
|
+
}
|
|
39269
|
+
return set;
|
|
39270
|
+
};
|
|
39271
|
+
for (const seed of seeds) {
|
|
39272
|
+
const base = seed.variable.replace(/^this\./, "");
|
|
39273
|
+
if (fieldNames.has(base)) taintedFields.add(base);
|
|
39274
|
+
else localsOf(methodOf(seed.line)).add(seed.variable);
|
|
39275
|
+
}
|
|
39276
|
+
const out2 = [];
|
|
39277
|
+
let changed = true;
|
|
39278
|
+
for (let guard = 0; changed && guard < lines.length + 2; guard++) {
|
|
39279
|
+
changed = false;
|
|
39280
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
39281
|
+
let text = lines[i2];
|
|
39282
|
+
const trimmed = text.trimStart();
|
|
39283
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
39284
|
+
if (/^\s*(?:[\w.<>\[\]?]+\s+)?[A-Za-z_]\w*\s*=(?!=)/.test(text) && !/;\s*(?:\/\/.*)?$/.test(text)) {
|
|
39285
|
+
for (let j = i2 + 1; j < lines.length && j <= i2 + 12; j++) {
|
|
39286
|
+
text = text.trimEnd() + " " + lines[j].trim();
|
|
39287
|
+
if (/;\s*(?:\/\/.*)?$/.test(lines[j])) break;
|
|
39288
|
+
}
|
|
39289
|
+
}
|
|
39290
|
+
const m = methodOf(i2 + 1);
|
|
39291
|
+
const here = localsOf(m);
|
|
39292
|
+
const ret = returnRe.exec(text);
|
|
39293
|
+
if (ret && m !== -1 && !returnsTainted.has(methods[m].name)) {
|
|
39294
|
+
const idents2 = codeOf(ret[1]).match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
39295
|
+
if (idents2.some((t) => here.has(t) || taintedFields.has(t))) {
|
|
39296
|
+
returnsTainted.add(methods[m].name);
|
|
39297
|
+
changed = true;
|
|
39298
|
+
}
|
|
39299
|
+
continue;
|
|
39300
|
+
}
|
|
39301
|
+
const d = declRe.exec(text) ?? assignRe.exec(text);
|
|
39302
|
+
if (!d) continue;
|
|
39303
|
+
const [, lhs, rhs] = d;
|
|
39304
|
+
if (KEYWORDS2.has(lhs)) continue;
|
|
39305
|
+
const isField = fieldNames.has(lhs) && !declRe.test(text);
|
|
39306
|
+
if (isField ? taintedFields.has(lhs) : here.has(lhs)) continue;
|
|
39307
|
+
const rhsCode = codeOf(rhs);
|
|
39308
|
+
const idents = rhsCode.match(/[\p{L}\p{N}_]+/gu) ?? [];
|
|
39309
|
+
const viaLocal = idents.some((t) => here.has(t));
|
|
39310
|
+
const viaField = idents.some((t) => taintedFields.has(t) && !here.has(t));
|
|
39311
|
+
const viaCall = [...rhsCode.matchAll(/([\p{L}_][\p{L}\p{N}_]*)\s*\(/gu)].some((c) => returnsTainted.has(c[1]));
|
|
39312
|
+
if (!viaLocal && !viaField && !viaCall) continue;
|
|
39313
|
+
if (isField) taintedFields.add(lhs);
|
|
39314
|
+
else here.add(lhs);
|
|
39315
|
+
out2.push({ variable: lhs, line: i2 + 1, carried: !viaLocal });
|
|
39316
|
+
changed = true;
|
|
39317
|
+
}
|
|
39318
|
+
}
|
|
39319
|
+
return out2;
|
|
39320
|
+
}
|
|
38638
39321
|
var _reassignSplitCode;
|
|
38639
39322
|
var _reassignSplitLines;
|
|
38640
39323
|
function splitLinesCached(code) {
|
|
@@ -38643,7 +39326,7 @@ function splitLinesCached(code) {
|
|
|
38643
39326
|
_reassignSplitLines = code.split("\n");
|
|
38644
39327
|
return _reassignSplitLines;
|
|
38645
39328
|
}
|
|
38646
|
-
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
39329
|
+
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
|
|
38647
39330
|
if (!variable || sinkLine - srcLine < 2) return false;
|
|
38648
39331
|
if (!/^[A-Za-z_][\w]*$/.test(variable)) return false;
|
|
38649
39332
|
const lines = splitLinesCached(code);
|
|
@@ -38664,40 +39347,103 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
|
38664
39347
|
if (!line) continue;
|
|
38665
39348
|
if (reGuarded.test(line)) return true;
|
|
38666
39349
|
if (!reNaked.test(line) && !reSwitchCase.test(line)) continue;
|
|
38667
|
-
if (
|
|
39350
|
+
if (siblingBranchAssignsNonLiteral(lines, i2, variable, strLit, unreachableLines)) continue;
|
|
38668
39351
|
return true;
|
|
38669
39352
|
}
|
|
38670
39353
|
return false;
|
|
38671
39354
|
}
|
|
38672
|
-
function
|
|
39355
|
+
function enclosingBrace(lines, line, col) {
|
|
38673
39356
|
let depth = 0;
|
|
38674
|
-
let
|
|
38675
|
-
|
|
38676
|
-
|
|
38677
|
-
|
|
38678
|
-
if (
|
|
38679
|
-
|
|
39357
|
+
for (let i2 = line; i2 >= 0; i2--) {
|
|
39358
|
+
const text = lines[i2];
|
|
39359
|
+
for (let c = i2 === line ? Math.min(col, text.length) - 1 : text.length - 1; c >= 0; c--) {
|
|
39360
|
+
if (text[c] === "}") depth++;
|
|
39361
|
+
else if (text[c] === "{") {
|
|
39362
|
+
if (depth === 0) {
|
|
39363
|
+
let header = text.slice(0, c).trim();
|
|
39364
|
+
let headerLine = i2;
|
|
39365
|
+
for (let h = i2 - 1; header === "" && h >= 0; h--) {
|
|
39366
|
+
header = lines[h].trim();
|
|
39367
|
+
headerLine = h;
|
|
39368
|
+
}
|
|
39369
|
+
return { line: i2, col: c, header, headerLine };
|
|
39370
|
+
}
|
|
39371
|
+
depth--;
|
|
39372
|
+
}
|
|
38680
39373
|
}
|
|
38681
|
-
|
|
38682
|
-
|
|
38683
|
-
|
|
39374
|
+
}
|
|
39375
|
+
return null;
|
|
39376
|
+
}
|
|
39377
|
+
function matchingClose(lines, line, col) {
|
|
39378
|
+
let depth = 0;
|
|
39379
|
+
for (let i2 = line; i2 < lines.length; i2++) {
|
|
39380
|
+
const text = lines[i2];
|
|
39381
|
+
for (let c = i2 === line ? col : 0; c < text.length; c++) {
|
|
39382
|
+
if (text[c] === "{") depth++;
|
|
39383
|
+
else if (text[c] === "}" && --depth === 0) return { line: i2, col: c };
|
|
38684
39384
|
}
|
|
38685
39385
|
}
|
|
38686
|
-
|
|
39386
|
+
return null;
|
|
39387
|
+
}
|
|
39388
|
+
function siblingBranchAssignsNonLiteral(lines, idx, variable, strLit, unreachableLines) {
|
|
39389
|
+
let block = enclosingBrace(lines, idx, 0);
|
|
39390
|
+
if (block && /^(?:case\b.*|default\s*):$/.test(block.header)) {
|
|
39391
|
+
block = enclosingBrace(lines, block.line, block.col);
|
|
39392
|
+
}
|
|
39393
|
+
if (!block) return false;
|
|
39394
|
+
let first = block.headerLine;
|
|
39395
|
+
let last;
|
|
39396
|
+
const end = matchingClose(lines, block.line, block.col);
|
|
39397
|
+
if (!end) return false;
|
|
39398
|
+
last = end.line;
|
|
39399
|
+
if (/\bswitch\b/.test(block.header)) {
|
|
39400
|
+
} else if (/^(?:\}\s*)?else\b/.test(block.header) || /^(?:\}\s*)?(?:else\s+)?if\s*\(/.test(block.header)) {
|
|
39401
|
+
let cur = block;
|
|
39402
|
+
for (let guard = 0; guard < 64 && /^(?:\}\s*)?else\b/.test(cur.header); guard++) {
|
|
39403
|
+
let closeLine = cur.headerLine;
|
|
39404
|
+
let closeCol = lines[closeLine].indexOf("}");
|
|
39405
|
+
if (closeCol === -1 || closeCol > lines[closeLine].search(/\belse\b/)) {
|
|
39406
|
+
closeLine--;
|
|
39407
|
+
while (closeLine >= 0 && lines[closeLine].trim() === "") closeLine--;
|
|
39408
|
+
if (closeLine < 0 || !lines[closeLine].trimEnd().endsWith("}")) return false;
|
|
39409
|
+
closeCol = lines[closeLine].lastIndexOf("}");
|
|
39410
|
+
}
|
|
39411
|
+
const prev = enclosingBrace(lines, closeLine, closeCol);
|
|
39412
|
+
if (!prev) return false;
|
|
39413
|
+
cur = prev;
|
|
39414
|
+
first = prev.headerLine;
|
|
39415
|
+
}
|
|
39416
|
+
let close = end;
|
|
39417
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
39418
|
+
let l = close.line;
|
|
39419
|
+
let rest = lines[l].slice(close.col + 1).trim();
|
|
39420
|
+
while (rest === "" && l + 1 < lines.length) {
|
|
39421
|
+
l++;
|
|
39422
|
+
rest = lines[l].trim();
|
|
39423
|
+
}
|
|
39424
|
+
if (!/^else\b/.test(rest)) break;
|
|
39425
|
+
let openLine = l;
|
|
39426
|
+
let openCol = lines[l].indexOf("{", l === close.line ? close.col + 1 : 0);
|
|
39427
|
+
if (openCol === -1) {
|
|
39428
|
+
openLine = l + 1;
|
|
39429
|
+
while (openLine < lines.length && lines[openLine].trim() === "") openLine++;
|
|
39430
|
+
if (openLine >= lines.length || !lines[openLine].trimStart().startsWith("{")) break;
|
|
39431
|
+
openCol = lines[openLine].indexOf("{");
|
|
39432
|
+
}
|
|
39433
|
+
const next = matchingClose(lines, openLine, openCol);
|
|
39434
|
+
if (!next) break;
|
|
39435
|
+
close = next;
|
|
39436
|
+
last = next.line;
|
|
39437
|
+
}
|
|
39438
|
+
} else {
|
|
39439
|
+
return false;
|
|
39440
|
+
}
|
|
38687
39441
|
const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
|
|
38688
39442
|
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;
|
|
39443
|
+
for (let i2 = first; i2 <= last; i2++) {
|
|
39444
|
+
if (i2 === idx || unreachableLines?.has(i2 + 1)) continue;
|
|
39445
|
+
const m = assign.exec(lines[i2]);
|
|
39446
|
+
if (m && m[1] && !literalOnly.test(m[1])) return true;
|
|
38701
39447
|
}
|
|
38702
39448
|
return false;
|
|
38703
39449
|
}
|
|
@@ -38825,6 +39571,18 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38825
39571
|
const goFnStart = (line) => enclosingStart(goFnRanges, line);
|
|
38826
39572
|
const csFnRanges = methodRanges(["csharp"]);
|
|
38827
39573
|
const csFnStart = (line) => enclosingStart(csFnRanges, line);
|
|
39574
|
+
const csFieldNames = /* @__PURE__ */ new Set();
|
|
39575
|
+
if (language === "csharp") {
|
|
39576
|
+
for (const t of types ?? []) for (const f of t.fields ?? []) csFieldNames.add(f.name);
|
|
39577
|
+
}
|
|
39578
|
+
const csDifferentMethod = (variable, sourceLine, otherLine) => {
|
|
39579
|
+
if (csFnRanges.length === 0) return false;
|
|
39580
|
+
const a = csFnStart(sourceLine);
|
|
39581
|
+
const b = csFnStart(otherLine);
|
|
39582
|
+
if (a === -1 || b === -1 || a === b) return false;
|
|
39583
|
+
const base = variable.replace(/^this\./, "").split(".")[0];
|
|
39584
|
+
return !csFieldNames.has(base);
|
|
39585
|
+
};
|
|
38828
39586
|
const sourcesWithVar = sources.filter(
|
|
38829
39587
|
(s) => typeof s.variable === "string" && s.variable.length > 0
|
|
38830
39588
|
);
|
|
@@ -39072,7 +39830,28 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39072
39830
|
}
|
|
39073
39831
|
}
|
|
39074
39832
|
}
|
|
39075
|
-
if (
|
|
39833
|
+
if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0 && csFnRanges.length > 0) {
|
|
39834
|
+
const csMethods = (types ?? []).flatMap((t) => t.methods);
|
|
39835
|
+
const seeds = sourcesWithVar.map((sv) => ({ variable: sv.variable, line: sv.line }));
|
|
39836
|
+
const methodStartOf = (line) => csFnStart(line);
|
|
39837
|
+
const aliases = csharpMethodScopedAliases(code, seeds, csMethods, csFieldNames);
|
|
39838
|
+
const fileAnchor = sourcesWithVar.reduce((a, sv) => sv.line < a.line ? sv : a, sourcesWithVar[0]);
|
|
39839
|
+
const have = new Set(sourcesWithVar.map((sv) => `${sv.variable}@${methodStartOf(sv.line)}`));
|
|
39840
|
+
for (const alias of aliases) {
|
|
39841
|
+
const fn = methodStartOf(alias.line);
|
|
39842
|
+
const isField = csFieldNames.has(alias.variable);
|
|
39843
|
+
const key = `${alias.variable}@${isField ? "field" : fn}`;
|
|
39844
|
+
if (have.has(key)) continue;
|
|
39845
|
+
have.add(key);
|
|
39846
|
+
const inMethod = sourcesWithVar.filter((sv) => methodStartOf(sv.line) === fn && sv.line <= alias.line);
|
|
39847
|
+
const from = !alias.carried && inMethod.length > 0 ? inMethod.reduce((a, sv) => sv.line < a.line ? sv : a, inMethod[0]) : fileAnchor;
|
|
39848
|
+
sourcesWithVar.push({
|
|
39849
|
+
...from,
|
|
39850
|
+
variable: alias.variable,
|
|
39851
|
+
...alias.carried || inMethod.length === 0 ? { line: alias.line } : {}
|
|
39852
|
+
});
|
|
39853
|
+
}
|
|
39854
|
+
} else if ((language === "java" || language === "csharp") && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
39076
39855
|
const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
39077
39856
|
const derived = buildJavaTaintedVars(code, seedVars);
|
|
39078
39857
|
if (derived.size > 0) {
|
|
@@ -39114,20 +39893,25 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39114
39893
|
if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
39115
39894
|
const lines = code.split("\n");
|
|
39116
39895
|
const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
|
|
39117
|
-
const
|
|
39896
|
+
const seeded = /* @__PURE__ */ new Set();
|
|
39118
39897
|
let added = true;
|
|
39119
39898
|
let guard = 0;
|
|
39120
39899
|
while (added && guard < lines.length + 2) {
|
|
39121
39900
|
added = false;
|
|
39122
39901
|
guard++;
|
|
39123
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
39124
39902
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
39125
39903
|
const m = ctRe.exec(lines[i2]);
|
|
39126
39904
|
if (!m) continue;
|
|
39127
39905
|
const [, obj, rhs] = m;
|
|
39128
|
-
|
|
39129
|
-
if (
|
|
39130
|
-
sourcesWithVar.
|
|
39906
|
+
const key = `${obj}@${csFnStart(i2 + 1)}`;
|
|
39907
|
+
if (seeded.has(key)) continue;
|
|
39908
|
+
const reachable = sourcesWithVar.filter(
|
|
39909
|
+
(sv) => sv.line <= i2 + 1 && !csDifferentMethod(sv.variable, sv.line, i2 + 1)
|
|
39910
|
+
);
|
|
39911
|
+
if (reachable.length === 0 || !argsReferenceTaintedVar(rhs, reachable)) continue;
|
|
39912
|
+
const from = reachable.reduce((a, sv) => sv.line < a.line ? sv : a, reachable[0]);
|
|
39913
|
+
sourcesWithVar.push({ ...from, variable: obj, line: i2 + 1 });
|
|
39914
|
+
seeded.add(key);
|
|
39131
39915
|
added = true;
|
|
39132
39916
|
}
|
|
39133
39917
|
}
|
|
@@ -39143,17 +39927,18 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39143
39927
|
if (builders.size > 0) {
|
|
39144
39928
|
const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
|
|
39145
39929
|
const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
|
|
39930
|
+
const builderSeeded = /* @__PURE__ */ new Set();
|
|
39146
39931
|
let added = true;
|
|
39147
39932
|
let guard = 0;
|
|
39148
39933
|
while (added && guard < lines.length + 2) {
|
|
39149
39934
|
added = false;
|
|
39150
39935
|
guard++;
|
|
39151
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
39152
39936
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
39153
39937
|
const m = appendRe.exec(lines[i2]);
|
|
39154
39938
|
if (!m) continue;
|
|
39155
39939
|
const [, obj, args2] = m;
|
|
39156
|
-
|
|
39940
|
+
const seedKey = `${obj}@${csFnStart(i2 + 1)}`;
|
|
39941
|
+
if (!builders.has(obj) || builderSeeded.has(seedKey)) continue;
|
|
39157
39942
|
const appendFn = csFnStart(i2 + 1);
|
|
39158
39943
|
const reachable = sourcesWithVar.filter(
|
|
39159
39944
|
(sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn
|
|
@@ -39161,6 +39946,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39161
39946
|
if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
|
|
39162
39947
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
|
|
39163
39948
|
builderSeeds.add(`${obj}@${i2 + 1}`);
|
|
39949
|
+
builderSeeded.add(seedKey);
|
|
39164
39950
|
added = true;
|
|
39165
39951
|
}
|
|
39166
39952
|
}
|
|
@@ -39234,6 +40020,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39234
40020
|
const sinkFn = csFnStart(sink.line);
|
|
39235
40021
|
if (srcFn !== sinkFn) continue;
|
|
39236
40022
|
}
|
|
40023
|
+
if (csDifferentMethod(source.variable, source.line, sink.line)) continue;
|
|
39237
40024
|
if (complex) {
|
|
39238
40025
|
const re = reCache.get(source.variable);
|
|
39239
40026
|
if (!re || !re.test(expr)) continue;
|
|
@@ -39244,7 +40031,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39244
40031
|
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
39245
40032
|
break;
|
|
39246
40033
|
}
|
|
39247
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
|
|
40034
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line, unreachableLines)) {
|
|
39248
40035
|
break;
|
|
39249
40036
|
}
|
|
39250
40037
|
flows.push({
|
|
@@ -39380,6 +40167,7 @@ var InterproceduralPass = class {
|
|
|
39380
40167
|
}
|
|
39381
40168
|
}
|
|
39382
40169
|
const language = graph.ir.meta.language;
|
|
40170
|
+
const csCredit = language === "csharp" && typeof ctx.code === "string" ? new CSharpCredit(ctx.code, graph.ir.types.flatMap((t) => t.methods)) : null;
|
|
39383
40171
|
if (sinks.length > 0) {
|
|
39384
40172
|
const interProc = analyzeInterprocedural(graph, sources, sinks, sanitizers, {
|
|
39385
40173
|
taintedVariables: constProp.tainted
|
|
@@ -39406,6 +40194,9 @@ var InterproceduralPass = class {
|
|
|
39406
40194
|
if (!method) continue;
|
|
39407
40195
|
if (sink.line < method.startLine || sink.line > method.endLine) continue;
|
|
39408
40196
|
if (sanitizerMethodNames.has(method.name)) continue;
|
|
40197
|
+
if (language === "csharp" && typeof ctx.code === "string" && !csharpParamReachesSink(ctx.code, method, edge.taintedArgs, sink, csCredit)) {
|
|
40198
|
+
continue;
|
|
40199
|
+
}
|
|
39409
40200
|
for (const source of sources) {
|
|
39410
40201
|
if (source.line > edge.callLine) continue;
|
|
39411
40202
|
if (source.type === "interprocedural_param" && source.confidence < 0.6) continue;
|
|
@@ -39548,9 +40339,54 @@ var InterproceduralPass = class {
|
|
|
39548
40339
|
return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
|
|
39549
40340
|
});
|
|
39550
40341
|
}
|
|
40342
|
+
if (csCredit) {
|
|
40343
|
+
const csSeeds = sources.flatMap((s) => s.variable ? [{ variable: s.variable, line: s.line }] : []);
|
|
40344
|
+
const credited = /* @__PURE__ */ new Set();
|
|
40345
|
+
filteredAdditionalFlows = filteredAdditionalFlows.filter((f) => {
|
|
40346
|
+
if (f.sink_type !== "external_taint_escape" || !csCredit.creditedOnLine(f.sink_line, csSeeds)) return true;
|
|
40347
|
+
credited.add(f.sink_line);
|
|
40348
|
+
return false;
|
|
40349
|
+
});
|
|
40350
|
+
filteredAdditionalSinks = filteredAdditionalSinks.filter(
|
|
40351
|
+
(s) => !(s.type === "external_taint_escape" && credited.has(s.line))
|
|
40352
|
+
);
|
|
40353
|
+
}
|
|
39551
40354
|
return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
|
|
39552
40355
|
}
|
|
39553
40356
|
};
|
|
40357
|
+
function csharpParamReachesSink(code, method, taintedArgs, sink, credit) {
|
|
40358
|
+
const lines = code.split(/\r?\n/);
|
|
40359
|
+
const params = taintedArgs.length > 0 ? method.parameters.filter((p) => taintedArgs.includes(p.position)) : method.parameters;
|
|
40360
|
+
if (params.length === 0) return true;
|
|
40361
|
+
const derived = new Set(params.map((p) => p.name));
|
|
40362
|
+
const codeOf = (text) => text.replace(/\$@?"(?:[^"\\]|\\.)*"/g, (lit) => " " + [...lit.matchAll(/\{([^}]*)\}/g)].map((x) => x[1]).join(" ") + " ").replace(/@?"(?:[^"\\]|\\.)*"/g, " ").replace(/'(?:[^'\\]|\\.)'/g, " ");
|
|
40363
|
+
const mentionsDerived = (text) => {
|
|
40364
|
+
const bare = codeOf(text);
|
|
40365
|
+
return [...derived].filter((v) => new RegExp(`(?<![\\w.])${v}(?![\\w])`).test(bare));
|
|
40366
|
+
};
|
|
40367
|
+
const assignRe = /^\s*(?:var\s+|[A-Za-z_][\w.<>[\]?]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.+)$/;
|
|
40368
|
+
const objectWriteRe = /^\s*([A-Za-z_]\w*)\s*\.\s*[A-Za-z_]\w*\s*(?:\+?=(?!=)|\()(.*)$/;
|
|
40369
|
+
for (let i2 = method.startLine + 1; i2 < sink.line; i2++) {
|
|
40370
|
+
const text = lines[i2 - 1] ?? "";
|
|
40371
|
+
const assign = assignRe.exec(text);
|
|
40372
|
+
if (assign) {
|
|
40373
|
+
if (mentionsDerived(assign[2]).length > 0) derived.add(assign[1]);
|
|
40374
|
+
else if (!params.some((p) => p.name === assign[1])) derived.delete(assign[1]);
|
|
40375
|
+
continue;
|
|
40376
|
+
}
|
|
40377
|
+
const write = objectWriteRe.exec(text);
|
|
40378
|
+
if (write && mentionsDerived(write[2]).length > 0) derived.add(write[1]);
|
|
40379
|
+
}
|
|
40380
|
+
let statement = lines[sink.line - 1] ?? "";
|
|
40381
|
+
for (let i2 = sink.line; i2 < lines.length && i2 < sink.line + 6 && !/[;{}]\s*$/.test(statement.trimEnd()); i2++) {
|
|
40382
|
+
statement += " " + (lines[i2] ?? "").trim();
|
|
40383
|
+
}
|
|
40384
|
+
const reaching = mentionsDerived(statement);
|
|
40385
|
+
if (reaching.length === 0) return false;
|
|
40386
|
+
if (!credit) return true;
|
|
40387
|
+
const safe = /* @__PURE__ */ new Set([...credit.sanitizedVarsAt(sink.type, sink.line), ...credit.guardedVarsAt(sink.type, sink.line)]);
|
|
40388
|
+
return reaching.some((v) => !safe.has(v));
|
|
40389
|
+
}
|
|
39554
40390
|
|
|
39555
40391
|
// src/analysis/passes/dead-code-pass.ts
|
|
39556
40392
|
var DeadCodePass = class {
|
|
@@ -40234,7 +41070,7 @@ var NullDerefPass = class {
|
|
|
40234
41070
|
};
|
|
40235
41071
|
|
|
40236
41072
|
// src/analysis/passes/resource-leak-pass.ts
|
|
40237
|
-
function
|
|
41073
|
+
function escapeRegex3(s) {
|
|
40238
41074
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40239
41075
|
}
|
|
40240
41076
|
var FACTORY_METHOD_NAME_RE = /^(?:open|create|new|get|make|build)[A-Z]/;
|
|
@@ -40453,7 +41289,7 @@ var ResourceLeakPass = class {
|
|
|
40453
41289
|
* ownership to the caller (caller is responsible for close).
|
|
40454
41290
|
*/
|
|
40455
41291
|
isReturnedToCaller(lines, variable, fromLine, toLine) {
|
|
40456
|
-
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${
|
|
41292
|
+
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex3(variable)}\\b`);
|
|
40457
41293
|
for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
|
|
40458
41294
|
if (varRe.test(lines[l - 1] ?? "")) return true;
|
|
40459
41295
|
}
|
|
@@ -40466,7 +41302,7 @@ var ResourceLeakPass = class {
|
|
|
40466
41302
|
*/
|
|
40467
41303
|
fieldStoredName(lines, variable, openLine, toLine) {
|
|
40468
41304
|
const thisAssignRe = new RegExp(
|
|
40469
|
-
`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${
|
|
41305
|
+
`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`
|
|
40470
41306
|
);
|
|
40471
41307
|
for (let l = openLine; l <= toLine && l <= lines.length; l++) {
|
|
40472
41308
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|
|
@@ -40541,7 +41377,7 @@ var ResourceLeakPass = class {
|
|
|
40541
41377
|
candidateField = variable;
|
|
40542
41378
|
} else {
|
|
40543
41379
|
const thisAssignRe = new RegExp(
|
|
40544
|
-
`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${
|
|
41380
|
+
`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`
|
|
40545
41381
|
);
|
|
40546
41382
|
for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
|
|
40547
41383
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|