circle-ir 4.11.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/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +8 -2
- package/dist/analysis/config-loader.js.map +1 -1
- 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 +56 -2
- package/dist/analysis/constant-propagation/propagator.js.map +1 -1
- package/dist/analysis/entry-point-detection.d.ts +7 -0
- package/dist/analysis/entry-point-detection.d.ts.map +1 -1
- package/dist/analysis/entry-point-detection.js +13 -1
- package/dist/analysis/entry-point-detection.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/language-sources-pass.d.ts +6 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +15 -2
- package/dist/analysis/passes/language-sources-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 +348 -16
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/require-entry-path.js +50 -4
- package/dist/analysis/require-entry-path.js.map +1 -1
- package/dist/browser/circle-ir.js +861 -65
- package/dist/core/circle-ir-core.cjs +81 -4
- package/dist/core/circle-ir-core.js +81 -4
- package/dist/core/extractors/dfg.js +49 -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;
|
|
@@ -9970,6 +9978,30 @@ function processStatement2(stmt, startDefId, startUseId, scopeStack, isJavaScrip
|
|
|
9970
9978
|
}
|
|
9971
9979
|
break;
|
|
9972
9980
|
}
|
|
9981
|
+
case "switch_expression":
|
|
9982
|
+
case "switch_statement": {
|
|
9983
|
+
const value = stmt.childForFieldName("condition") ?? stmt.childForFieldName("value");
|
|
9984
|
+
if (value) {
|
|
9985
|
+
const valueUses = extractUses(value, useId, scopeStack, isJavaScript);
|
|
9986
|
+
uses.push(...valueUses.uses);
|
|
9987
|
+
useId = valueUses.nextId;
|
|
9988
|
+
}
|
|
9989
|
+
const body2 = stmt.childForFieldName("body");
|
|
9990
|
+
if (body2) {
|
|
9991
|
+
scopeStack.push(/* @__PURE__ */ new Map());
|
|
9992
|
+
for (let i2 = 0; i2 < body2.childCount; i2++) {
|
|
9993
|
+
const group = body2.child(i2);
|
|
9994
|
+
if (!group) continue;
|
|
9995
|
+
const result = processBlock(group, defId, useId, scopeStack, isJavaScript);
|
|
9996
|
+
defs.push(...result.defs);
|
|
9997
|
+
uses.push(...result.uses);
|
|
9998
|
+
defId = result.nextDefId;
|
|
9999
|
+
useId = result.nextUseId;
|
|
10000
|
+
}
|
|
10001
|
+
scopeStack.pop();
|
|
10002
|
+
}
|
|
10003
|
+
break;
|
|
10004
|
+
}
|
|
9973
10005
|
case "block":
|
|
9974
10006
|
case "statement_block": {
|
|
9975
10007
|
scopeStack.push(/* @__PURE__ */ new Map());
|
|
@@ -11997,6 +12029,7 @@ var DEFAULT_SOURCES = [
|
|
|
11997
12029
|
{ method: "get", class: "ConcurrentHashMap", type: "plugin_param", severity: "high", return_tainted: true },
|
|
11998
12030
|
{ method: "get", class: "Hashtable", type: "plugin_param", severity: "high", return_tainted: true },
|
|
11999
12031
|
{ method: "get", class: "Properties", type: "config_param", severity: "high", return_tainted: true },
|
|
12032
|
+
{ method: "getProperty", class: "Properties", type: "config_param", severity: "high", return_tainted: true },
|
|
12000
12033
|
// Message/Event sources
|
|
12001
12034
|
{ method: "getText", class: "Message", type: "io_input", severity: "high", return_tainted: true },
|
|
12002
12035
|
{ method: "getPayload", type: "io_input", severity: "high", return_tainted: true },
|
|
@@ -12974,8 +13007,13 @@ var DEFAULT_SINKS = [
|
|
|
12974
13007
|
{ method: "write", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
12975
13008
|
{ method: "println", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
12976
13009
|
{ method: "print", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
12977
|
-
|
|
12978
|
-
|
|
13010
|
+
// format/printf take `(String fmt, Object... args)` or `(Locale l, String
|
|
13011
|
+
// fmt, Object... args)`. Every argument after the format can reach the page,
|
|
13012
|
+
// and the Locale overload shifts them all one place right, so `[0, 1]` missed
|
|
13013
|
+
// `printf(Locale.US, "%s", tainted)` and any second vararg. Enumerated, not
|
|
13014
|
+
// `[]` — see the ProcessBuilder note above.
|
|
13015
|
+
{ method: "format", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
13016
|
+
{ method: "printf", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1, 2, 3, 4, 5, 6, 7] },
|
|
12979
13017
|
// ServletOutputStream
|
|
12980
13018
|
{ method: "write", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
12981
13019
|
{ method: "print", class: "ServletOutputStream", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0] },
|
|
@@ -19703,7 +19741,16 @@ var CSHARP_NON_ESCAPE_METHODS = /* @__PURE__ */ new Set([
|
|
|
19703
19741
|
"TryAdd",
|
|
19704
19742
|
"Insert",
|
|
19705
19743
|
"Push",
|
|
19706
|
-
"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"
|
|
19707
19754
|
]);
|
|
19708
19755
|
function isCSharpNonEscapeCall(call) {
|
|
19709
19756
|
const name2 = call.method_name;
|
|
@@ -22121,7 +22168,16 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22121
22168
|
break;
|
|
22122
22169
|
}
|
|
22123
22170
|
}
|
|
22124
|
-
|
|
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
|
+
);
|
|
22125
22181
|
if (!switchBlock) {
|
|
22126
22182
|
for (const child of node.children) {
|
|
22127
22183
|
this.visit(child);
|
|
@@ -22129,7 +22185,7 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22129
22185
|
return;
|
|
22130
22186
|
}
|
|
22131
22187
|
const caseGroups = switchBlock.children.filter(
|
|
22132
|
-
(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"
|
|
22133
22189
|
);
|
|
22134
22190
|
if (switchValue && isKnown(switchValue)) {
|
|
22135
22191
|
let matchingIdx = -1;
|
|
@@ -22147,6 +22203,16 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22147
22203
|
matchingIdx = i2;
|
|
22148
22204
|
}
|
|
22149
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
|
+
}
|
|
22150
22216
|
}
|
|
22151
22217
|
}
|
|
22152
22218
|
}
|
|
@@ -22167,9 +22233,17 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22167
22233
|
}
|
|
22168
22234
|
}
|
|
22169
22235
|
} else {
|
|
22236
|
+
const taintedBefore = new Set(this.tainted);
|
|
22237
|
+
const taintedAfter = new Set(taintedBefore);
|
|
22238
|
+
const wasInConditional = this.inConditionalBranch;
|
|
22239
|
+
this.inConditionalBranch = true;
|
|
22170
22240
|
for (const caseGroup of caseGroups) {
|
|
22241
|
+
this.tainted = new Set(taintedBefore);
|
|
22171
22242
|
this.visit(caseGroup);
|
|
22243
|
+
for (const v of this.tainted) taintedAfter.add(v);
|
|
22172
22244
|
}
|
|
22245
|
+
this.inConditionalBranch = wasInConditional;
|
|
22246
|
+
this.tainted = taintedAfter;
|
|
22173
22247
|
}
|
|
22174
22248
|
}
|
|
22175
22249
|
handleTernary(node) {
|
|
@@ -22220,6 +22294,18 @@ var ConstantPropagator = class _ConstantPropagator {
|
|
|
22220
22294
|
}
|
|
22221
22295
|
return false;
|
|
22222
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
|
+
}
|
|
22223
22309
|
extractCaseValue(labelNode) {
|
|
22224
22310
|
for (const child of labelNode.children) {
|
|
22225
22311
|
if (child.type === "decimal_integer_literal") {
|
|
@@ -23333,7 +23419,8 @@ function annotationsInclude(annotations, targets) {
|
|
|
23333
23419
|
return false;
|
|
23334
23420
|
}
|
|
23335
23421
|
function simpleTypeName(ref) {
|
|
23336
|
-
|
|
23422
|
+
const name2 = ref.replace(/<.*$/, "").trim();
|
|
23423
|
+
return name2.substring(name2.lastIndexOf(".") + 1);
|
|
23337
23424
|
}
|
|
23338
23425
|
function looksLikeMainMethod(method) {
|
|
23339
23426
|
if (method.name !== "main") return false;
|
|
@@ -28173,7 +28260,10 @@ var LanguageSourcesPass = class {
|
|
|
28173
28260
|
for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
|
|
28174
28261
|
ctx.addFinding(finding);
|
|
28175
28262
|
}
|
|
28176
|
-
|
|
28263
|
+
const xssSinkLines = new Set(
|
|
28264
|
+
ctx.getResult("taint-matcher").sinks.filter((s) => s.type === "xss").map((s) => s.line)
|
|
28265
|
+
);
|
|
28266
|
+
for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file, xssSinkLines)) {
|
|
28177
28267
|
ctx.addFinding(finding);
|
|
28178
28268
|
}
|
|
28179
28269
|
for (const finding of findJavaMongoNosqlInjectionFindings(
|
|
@@ -31275,7 +31365,7 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
31275
31365
|
if (!m) continue;
|
|
31276
31366
|
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
31277
31367
|
const guarded = /* @__PURE__ */ new Set([m[1]]);
|
|
31278
|
-
const
|
|
31368
|
+
const mentions2 = (t) => {
|
|
31279
31369
|
for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
|
|
31280
31370
|
return false;
|
|
31281
31371
|
};
|
|
@@ -31283,10 +31373,10 @@ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
|
|
|
31283
31373
|
const lt = lines[l];
|
|
31284
31374
|
const a = assignRe.exec(lt);
|
|
31285
31375
|
if (a) {
|
|
31286
|
-
if (
|
|
31376
|
+
if (mentions2(a[2])) guarded.add(a[1]);
|
|
31287
31377
|
else if (a[1] !== m[1]) guarded.delete(a[1]);
|
|
31288
31378
|
}
|
|
31289
|
-
if (
|
|
31379
|
+
if (mentions2(lt)) {
|
|
31290
31380
|
sanitizers.push({
|
|
31291
31381
|
type: "java_canonical_startswith_guard",
|
|
31292
31382
|
method: "getCanonicalPath",
|
|
@@ -31342,14 +31432,14 @@ function findCSharpEarlyReturnGuardSanitizers(code, language) {
|
|
|
31342
31432
|
if (!a || !guarded.has(a[1])) continue;
|
|
31343
31433
|
for (const id of a[2].match(/[A-Za-z_]\w*/g) ?? []) guarded.add(id);
|
|
31344
31434
|
}
|
|
31345
|
-
const
|
|
31435
|
+
const mentions2 = (t) => {
|
|
31346
31436
|
for (const n of guarded) if (new RegExp(`\\b${n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`).test(t)) return true;
|
|
31347
31437
|
return false;
|
|
31348
31438
|
};
|
|
31349
31439
|
for (let l = i2 + 1; l < lines.length; l++) {
|
|
31350
31440
|
const a = assignRe.exec(lines[l]);
|
|
31351
|
-
if (a &&
|
|
31352
|
-
if (
|
|
31441
|
+
if (a && mentions2(a[2])) guarded.add(a[1]);
|
|
31442
|
+
if (mentions2(lines[l])) {
|
|
31353
31443
|
sanitizers.push({
|
|
31354
31444
|
type: "csharp_early_return_guard",
|
|
31355
31445
|
method: "guard",
|
|
@@ -31387,7 +31477,7 @@ function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
|
31387
31477
|
if (!m || !canonicalVars.has(m[1])) continue;
|
|
31388
31478
|
if (!guardRejects(lines, i2, terminatorRe, 6)) continue;
|
|
31389
31479
|
const guarded = /* @__PURE__ */ new Set([m[1]]);
|
|
31390
|
-
const
|
|
31480
|
+
const mentions2 = (t) => {
|
|
31391
31481
|
for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
|
|
31392
31482
|
return false;
|
|
31393
31483
|
};
|
|
@@ -31395,10 +31485,10 @@ function findCSharpFullPathStartsWithGuardSanitizers(code) {
|
|
|
31395
31485
|
const lt = lines[l];
|
|
31396
31486
|
const a = assignRe.exec(lt);
|
|
31397
31487
|
if (a) {
|
|
31398
|
-
if (
|
|
31488
|
+
if (mentions2(a[2])) guarded.add(a[1]);
|
|
31399
31489
|
else if (a[1] !== m[1]) guarded.delete(a[1]);
|
|
31400
31490
|
}
|
|
31401
|
-
if (
|
|
31491
|
+
if (mentions2(lt)) {
|
|
31402
31492
|
sanitizers.push({
|
|
31403
31493
|
type: "csharp_fullpath_startswith_guard",
|
|
31404
31494
|
method: "GetFullPath",
|
|
@@ -32230,7 +32320,7 @@ function findGoXssFindings(code, file) {
|
|
|
32230
32320
|
}
|
|
32231
32321
|
return out2;
|
|
32232
32322
|
}
|
|
32233
|
-
function findJavaResponseWriterXssFindings(code, file) {
|
|
32323
|
+
function findJavaResponseWriterXssFindings(code, file, taintSinkLines = /* @__PURE__ */ new Set()) {
|
|
32234
32324
|
const out2 = [];
|
|
32235
32325
|
const lines = code.split("\n");
|
|
32236
32326
|
const respNames = /* @__PURE__ */ new Set();
|
|
@@ -32258,6 +32348,7 @@ function findJavaResponseWriterXssFindings(code, file) {
|
|
|
32258
32348
|
const chainRe = /\b([A-Za-z_]\w*)\s*\.\s*getWriter\s*\(\s*\)\s*\.\s*(print|println|write|printf|format|append)\s*\(([\s\S]*)\)\s*;?\s*(?:\/\/.*)?$/;
|
|
32259
32349
|
const m = trimmed.match(chainRe);
|
|
32260
32350
|
if (!m) continue;
|
|
32351
|
+
if (taintSinkLines.has(i2 + 1)) continue;
|
|
32261
32352
|
const recv = m[1];
|
|
32262
32353
|
if (!respNames.has(recv)) continue;
|
|
32263
32354
|
const args2 = m[3];
|
|
@@ -35017,6 +35108,443 @@ var MyBatisAnnotationSqlSinkPass = class {
|
|
|
35017
35108
|
}
|
|
35018
35109
|
};
|
|
35019
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
|
+
|
|
35020
35548
|
// src/analysis/passes/sink-filter-pass.ts
|
|
35021
35549
|
function escapeReSf(s) {
|
|
35022
35550
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -35307,7 +35835,6 @@ function jsSsrfHostGuardedLines(code) {
|
|
|
35307
35835
|
}
|
|
35308
35836
|
return covered;
|
|
35309
35837
|
}
|
|
35310
|
-
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*\)/;
|
|
35311
35838
|
function isUnwrittenLocalMapKeyRead(code, source) {
|
|
35312
35839
|
if (source.type !== "plugin_param") return false;
|
|
35313
35840
|
const lines = code.split("\n");
|
|
@@ -35329,18 +35856,6 @@ function isUnwrittenLocalMapKeyRead(code, source) {
|
|
|
35329
35856
|
if (lines.some((l) => putRe.test(l))) return false;
|
|
35330
35857
|
return true;
|
|
35331
35858
|
}
|
|
35332
|
-
var CSHARP_SANITIZER_RES = [
|
|
35333
|
-
{ re: /\b(?:HtmlEncode|JavaScriptStringEncode)\s*\(/, type: "xss" },
|
|
35334
|
-
{ re: /\bHtmlEncoder\s*\.\s*Encode\s*\(/, type: "xss" },
|
|
35335
|
-
{ re: /\bPath\s*\.\s*GetFileName\s*\(/, type: "path_traversal" },
|
|
35336
|
-
// An allowlist strip leaves only inert characters, so it covers every family
|
|
35337
|
-
// whose exploitation needs a metacharacter. Each has a fixture; see
|
|
35338
|
-
// `issue-272-csharp-allowlist-strip.test.ts`.
|
|
35339
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "ldap_injection" },
|
|
35340
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "xpath_injection" },
|
|
35341
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "command_injection" },
|
|
35342
|
-
{ re: CSHARP_ALLOWLIST_STRIP_RE, type: "sql_injection" }
|
|
35343
|
-
];
|
|
35344
35859
|
function csharpSanitizedVarsByType(code) {
|
|
35345
35860
|
const lines = code.split("\n");
|
|
35346
35861
|
const byType = /* @__PURE__ */ new Map();
|
|
@@ -35368,7 +35883,7 @@ function csharpSanitizedVarsByType(code) {
|
|
|
35368
35883
|
for (const [type, set] of byType) {
|
|
35369
35884
|
if (set.has(lhs)) continue;
|
|
35370
35885
|
const refsSanitized = [...set].some(
|
|
35371
|
-
(v) => new RegExp(`(?<![\\w])${
|
|
35886
|
+
(v) => new RegExp(`(?<![\\w])${escapeRegex2(v)}(?![\\w])`).test(rhs)
|
|
35372
35887
|
);
|
|
35373
35888
|
if (refsSanitized) {
|
|
35374
35889
|
set.add(lhs);
|
|
@@ -36159,18 +36674,25 @@ var SinkFilterPass = class {
|
|
|
36159
36674
|
if (language === "csharp") {
|
|
36160
36675
|
const sourceLines = ctx.code.split("\n");
|
|
36161
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));
|
|
36162
36679
|
const xxeHardened = /\bXmlResolver\s*=\s*null\b/.test(ctx.code) || /\bDtdProcessing\s*=\s*(?:DtdProcessing\s*\.\s*)?(?:Prohibit|Ignore)\b/.test(ctx.code);
|
|
36163
36680
|
filtered = filtered.filter((sink) => {
|
|
36164
36681
|
if (sink.type === "xxe" && xxeHardened) return false;
|
|
36165
36682
|
const sinkLineText = sourceLines[sink.line - 1] ?? "";
|
|
36166
|
-
|
|
36167
|
-
|
|
36168
|
-
|
|
36169
|
-
const
|
|
36170
|
-
|
|
36171
|
-
|
|
36172
|
-
|
|
36173
|
-
|
|
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;
|
|
36174
36696
|
}
|
|
36175
36697
|
return true;
|
|
36176
36698
|
});
|
|
@@ -36178,7 +36700,7 @@ var SinkFilterPass = class {
|
|
|
36178
36700
|
if (language === "csharp") {
|
|
36179
36701
|
const sourceLines = ctx.code.split("\n");
|
|
36180
36702
|
const constBaseClients = /* @__PURE__ */ new Set();
|
|
36181
|
-
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*\)/;
|
|
36182
36704
|
for (const line of sourceLines) {
|
|
36183
36705
|
const m = declRe.exec(line);
|
|
36184
36706
|
if (m) constBaseClients.add(m[1]);
|
|
@@ -36190,13 +36712,20 @@ var SinkFilterPass = class {
|
|
|
36190
36712
|
const text = sourceLines[sink.line - 1] ?? "";
|
|
36191
36713
|
let onConstClient = false;
|
|
36192
36714
|
for (const v of constBaseClients) {
|
|
36193
|
-
if (new RegExp(`(?<![\\w])${
|
|
36715
|
+
if (new RegExp(`(?<![\\w])${escapeRegex2(v)}\\s*\\.`).test(text)) {
|
|
36194
36716
|
onConstClient = true;
|
|
36195
36717
|
break;
|
|
36196
36718
|
}
|
|
36197
36719
|
}
|
|
36198
36720
|
if (!onConstClient) return true;
|
|
36199
|
-
|
|
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
|
+
}
|
|
36200
36729
|
if (!m) return true;
|
|
36201
36730
|
const lit = m[1];
|
|
36202
36731
|
if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(lit)) return true;
|
|
@@ -36226,7 +36755,7 @@ var SinkFilterPass = class {
|
|
|
36226
36755
|
const allLdapSanitizers = [...PY_BUILTIN_LDAP_SANITIZERS, ...wrappers];
|
|
36227
36756
|
if (allLdapSanitizers.length > 0) {
|
|
36228
36757
|
const ldapPatterns = allLdapSanitizers.map(
|
|
36229
|
-
(name2) => new RegExp(`\\b${
|
|
36758
|
+
(name2) => new RegExp(`\\b${escapeRegex2(name2)}\\s*\\(`)
|
|
36230
36759
|
);
|
|
36231
36760
|
filtered = filtered.filter((sink) => {
|
|
36232
36761
|
if (sink.type !== "ldap_injection") return true;
|
|
@@ -36298,7 +36827,7 @@ var SinkFilterPass = class {
|
|
|
36298
36827
|
return { sources, sinks: filtered, sanitizers };
|
|
36299
36828
|
}
|
|
36300
36829
|
};
|
|
36301
|
-
function
|
|
36830
|
+
function escapeRegex2(s) {
|
|
36302
36831
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36303
36832
|
}
|
|
36304
36833
|
var RESERVED_KEYWORDS = /* @__PURE__ */ new Set([
|
|
@@ -36382,7 +36911,7 @@ function extractSourceIdentifiers(text) {
|
|
|
36382
36911
|
return out2;
|
|
36383
36912
|
}
|
|
36384
36913
|
function isAssignedFromSanitizerPattern(sourceLines, sinkLine, varName, sanitizerPatterns, lookback = 30) {
|
|
36385
|
-
const escapedVar =
|
|
36914
|
+
const escapedVar = escapeRegex2(varName);
|
|
36386
36915
|
const assignRe = new RegExp(
|
|
36387
36916
|
`(?:\\b(?:const|let|var|final|String)\\s+)?\\b${escapedVar}\\s*=\\s*(.+)`
|
|
36388
36917
|
);
|
|
@@ -37852,7 +38381,7 @@ var LibraryProfileXssGatePass = class {
|
|
|
37852
38381
|
|
|
37853
38382
|
// src/analysis/passes/taint-propagation-pass.ts
|
|
37854
38383
|
var CONTAINER_READ_SOURCES = /* @__PURE__ */ new Set(["plugin_param", "config_param"]);
|
|
37855
|
-
function makeConstPropVeto(constProp, graph, sources, code) {
|
|
38384
|
+
function makeConstPropVeto(constProp, graph, sources, code, language) {
|
|
37856
38385
|
const lines = typeof code === "string" ? code.split("\n") : [];
|
|
37857
38386
|
const selfReassign = /* @__PURE__ */ new Map();
|
|
37858
38387
|
const escape = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -37882,6 +38411,7 @@ function makeConstPropVeto(constProp, graph, sources, code) {
|
|
|
37882
38411
|
const r = isFalsePositive(constProp, step.line, step.variable);
|
|
37883
38412
|
if (!r.isFalsePositive) return false;
|
|
37884
38413
|
if (r.reason !== "variable_not_tainted") return true;
|
|
38414
|
+
if (language === "csharp") return false;
|
|
37885
38415
|
const lo = Math.min(sourceLine, sinkLine);
|
|
37886
38416
|
let d = defAt(step.variable, step.line);
|
|
37887
38417
|
let selfHops = 0;
|
|
@@ -37915,7 +38445,7 @@ var TaintPropagationPass = class {
|
|
|
37915
38445
|
return { flows: [] };
|
|
37916
38446
|
}
|
|
37917
38447
|
const propagationResult = propagateTaint(graph, sources, sinks, sanitizers);
|
|
37918
|
-
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code);
|
|
38448
|
+
const isVetoed = makeConstPropVeto(constProp, graph, sources, ctx.code, ctx.language);
|
|
37919
38449
|
const verifiedFlows = propagationResult.flows.filter((flow) => {
|
|
37920
38450
|
if (constProp.unreachableLines.has(flow.sink.line)) return false;
|
|
37921
38451
|
for (const step of flow.path) {
|
|
@@ -38414,7 +38944,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
38414
38944
|
if (source.variable && source.variable !== varName && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
38415
38945
|
continue;
|
|
38416
38946
|
}
|
|
38417
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
|
|
38947
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line, unreachableLines)) {
|
|
38418
38948
|
continue;
|
|
38419
38949
|
}
|
|
38420
38950
|
flows.push({
|
|
@@ -38454,7 +38984,7 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
|
|
|
38454
38984
|
if (source.variable && source.variable !== collectionVar && source.in_method && call.in_method && source.in_method !== call.in_method) {
|
|
38455
38985
|
continue;
|
|
38456
38986
|
}
|
|
38457
|
-
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
|
|
38987
|
+
if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line, unreachableLines)) {
|
|
38458
38988
|
continue;
|
|
38459
38989
|
}
|
|
38460
38990
|
flows.push({
|
|
@@ -38568,7 +39098,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
38568
39098
|
if (paramSource) {
|
|
38569
39099
|
const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
|
|
38570
39100
|
if (!exists) {
|
|
38571
|
-
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)) {
|
|
38572
39102
|
continue;
|
|
38573
39103
|
}
|
|
38574
39104
|
flows.push({
|
|
@@ -38592,6 +39122,85 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
|
|
|
38592
39122
|
void types;
|
|
38593
39123
|
return flows;
|
|
38594
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
|
+
}
|
|
38595
39204
|
var _reassignSplitCode;
|
|
38596
39205
|
var _reassignSplitLines;
|
|
38597
39206
|
function splitLinesCached(code) {
|
|
@@ -38600,7 +39209,7 @@ function splitLinesCached(code) {
|
|
|
38600
39209
|
_reassignSplitLines = code.split("\n");
|
|
38601
39210
|
return _reassignSplitLines;
|
|
38602
39211
|
}
|
|
38603
|
-
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
39212
|
+
function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine, unreachableLines) {
|
|
38604
39213
|
if (!variable || sinkLine - srcLine < 2) return false;
|
|
38605
39214
|
if (!/^[A-Za-z_][\w]*$/.test(variable)) return false;
|
|
38606
39215
|
const lines = splitLinesCached(code);
|
|
@@ -38619,7 +39228,105 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
|
|
|
38619
39228
|
for (let i2 = lo; i2 < hi; i2++) {
|
|
38620
39229
|
const line = lines[i2];
|
|
38621
39230
|
if (!line) continue;
|
|
38622
|
-
if (
|
|
39231
|
+
if (reGuarded.test(line)) return true;
|
|
39232
|
+
if (!reNaked.test(line) && !reSwitchCase.test(line)) continue;
|
|
39233
|
+
if (siblingBranchAssignsNonLiteral(lines, i2, variable, strLit, unreachableLines)) continue;
|
|
39234
|
+
return true;
|
|
39235
|
+
}
|
|
39236
|
+
return false;
|
|
39237
|
+
}
|
|
39238
|
+
function enclosingBrace(lines, line, col) {
|
|
39239
|
+
let depth = 0;
|
|
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
|
+
}
|
|
39256
|
+
}
|
|
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 };
|
|
39267
|
+
}
|
|
39268
|
+
}
|
|
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
|
+
}
|
|
39324
|
+
const assign = new RegExp(`(?:^|[^\\w.$])${variable}\\s*(?::?=)(?!=)\\s*(.*?)\\s*;?\\s*(?:break\\s*;?)?\\s*$`);
|
|
39325
|
+
const literalOnly = new RegExp(`^${strLit}$`);
|
|
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;
|
|
38623
39330
|
}
|
|
38624
39331
|
return false;
|
|
38625
39332
|
}
|
|
@@ -38747,6 +39454,18 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38747
39454
|
const goFnStart = (line) => enclosingStart(goFnRanges, line);
|
|
38748
39455
|
const csFnRanges = methodRanges(["csharp"]);
|
|
38749
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
|
+
};
|
|
38750
39469
|
const sourcesWithVar = sources.filter(
|
|
38751
39470
|
(s) => typeof s.variable === "string" && s.variable.length > 0
|
|
38752
39471
|
);
|
|
@@ -38994,7 +39713,28 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
38994
39713
|
}
|
|
38995
39714
|
}
|
|
38996
39715
|
}
|
|
38997
|
-
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) {
|
|
38998
39738
|
const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
38999
39739
|
const derived = buildJavaTaintedVars(code, seedVars);
|
|
39000
39740
|
if (derived.size > 0) {
|
|
@@ -39036,20 +39776,25 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39036
39776
|
if (language === "csharp" && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
39037
39777
|
const lines = code.split("\n");
|
|
39038
39778
|
const ctRe = /(\w+)\s*\.\s*CommandText\s*\+?=\s*(.+)/;
|
|
39039
|
-
const
|
|
39779
|
+
const seeded = /* @__PURE__ */ new Set();
|
|
39040
39780
|
let added = true;
|
|
39041
39781
|
let guard = 0;
|
|
39042
39782
|
while (added && guard < lines.length + 2) {
|
|
39043
39783
|
added = false;
|
|
39044
39784
|
guard++;
|
|
39045
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
39046
39785
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
39047
39786
|
const m = ctRe.exec(lines[i2]);
|
|
39048
39787
|
if (!m) continue;
|
|
39049
39788
|
const [, obj, rhs] = m;
|
|
39050
|
-
|
|
39051
|
-
if (
|
|
39052
|
-
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);
|
|
39053
39798
|
added = true;
|
|
39054
39799
|
}
|
|
39055
39800
|
}
|
|
@@ -39065,17 +39810,18 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39065
39810
|
if (builders.size > 0) {
|
|
39066
39811
|
const appendRe = /([\p{L}_][\p{L}\p{N}_]*)\s*\.\s*(?:Append|AppendLine|AppendFormat|Insert)\s*\(([\s\S]*)/u;
|
|
39067
39812
|
const anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
|
|
39813
|
+
const builderSeeded = /* @__PURE__ */ new Set();
|
|
39068
39814
|
let added = true;
|
|
39069
39815
|
let guard = 0;
|
|
39070
39816
|
while (added && guard < lines.length + 2) {
|
|
39071
39817
|
added = false;
|
|
39072
39818
|
guard++;
|
|
39073
|
-
const known = new Set(sourcesWithVar.map((s) => s.variable));
|
|
39074
39819
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
39075
39820
|
const m = appendRe.exec(lines[i2]);
|
|
39076
39821
|
if (!m) continue;
|
|
39077
39822
|
const [, obj, args2] = m;
|
|
39078
|
-
|
|
39823
|
+
const seedKey = `${obj}@${csFnStart(i2 + 1)}`;
|
|
39824
|
+
if (!builders.has(obj) || builderSeeded.has(seedKey)) continue;
|
|
39079
39825
|
const appendFn = csFnStart(i2 + 1);
|
|
39080
39826
|
const reachable = sourcesWithVar.filter(
|
|
39081
39827
|
(sv) => appendFn === -1 || csFnStart(sv.line) === -1 || csFnStart(sv.line) === appendFn
|
|
@@ -39083,6 +39829,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39083
39829
|
if (reachable.length > 0 && argsReferenceTaintedVar(args2, reachable)) {
|
|
39084
39830
|
sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
|
|
39085
39831
|
builderSeeds.add(`${obj}@${i2 + 1}`);
|
|
39832
|
+
builderSeeded.add(seedKey);
|
|
39086
39833
|
added = true;
|
|
39087
39834
|
}
|
|
39088
39835
|
}
|
|
@@ -39156,6 +39903,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39156
39903
|
const sinkFn = csFnStart(sink.line);
|
|
39157
39904
|
if (srcFn !== sinkFn) continue;
|
|
39158
39905
|
}
|
|
39906
|
+
if (csDifferentMethod(source.variable, source.line, sink.line)) continue;
|
|
39159
39907
|
if (complex) {
|
|
39160
39908
|
const re = reCache.get(source.variable);
|
|
39161
39909
|
if (!re || !re.test(expr)) continue;
|
|
@@ -39166,7 +39914,7 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
39166
39914
|
if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
|
|
39167
39915
|
break;
|
|
39168
39916
|
}
|
|
39169
|
-
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)) {
|
|
39170
39918
|
break;
|
|
39171
39919
|
}
|
|
39172
39920
|
flows.push({
|
|
@@ -39302,6 +40050,7 @@ var InterproceduralPass = class {
|
|
|
39302
40050
|
}
|
|
39303
40051
|
}
|
|
39304
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;
|
|
39305
40054
|
if (sinks.length > 0) {
|
|
39306
40055
|
const interProc = analyzeInterprocedural(graph, sources, sinks, sanitizers, {
|
|
39307
40056
|
taintedVariables: constProp.tainted
|
|
@@ -39328,6 +40077,9 @@ var InterproceduralPass = class {
|
|
|
39328
40077
|
if (!method) continue;
|
|
39329
40078
|
if (sink.line < method.startLine || sink.line > method.endLine) continue;
|
|
39330
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
|
+
}
|
|
39331
40083
|
for (const source of sources) {
|
|
39332
40084
|
if (source.line > edge.callLine) continue;
|
|
39333
40085
|
if (source.type === "interprocedural_param" && source.confidence < 0.6) continue;
|
|
@@ -39470,9 +40222,53 @@ var InterproceduralPass = class {
|
|
|
39470
40222
|
return !sanitizedSinkKeys.has(`${s.line}:${s.type}`);
|
|
39471
40223
|
});
|
|
39472
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
|
+
}
|
|
39473
40236
|
return { additionalSinks: filteredAdditionalSinks, additionalFlows: filteredAdditionalFlows, interprocedural };
|
|
39474
40237
|
}
|
|
39475
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
|
+
}
|
|
39476
40272
|
|
|
39477
40273
|
// src/analysis/passes/dead-code-pass.ts
|
|
39478
40274
|
var DeadCodePass = class {
|
|
@@ -40156,7 +40952,7 @@ var NullDerefPass = class {
|
|
|
40156
40952
|
};
|
|
40157
40953
|
|
|
40158
40954
|
// src/analysis/passes/resource-leak-pass.ts
|
|
40159
|
-
function
|
|
40955
|
+
function escapeRegex3(s) {
|
|
40160
40956
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
40161
40957
|
}
|
|
40162
40958
|
var FACTORY_METHOD_NAME_RE = /^(?:open|create|new|get|make|build)[A-Z]/;
|
|
@@ -40375,7 +41171,7 @@ var ResourceLeakPass = class {
|
|
|
40375
41171
|
* ownership to the caller (caller is responsible for close).
|
|
40376
41172
|
*/
|
|
40377
41173
|
isReturnedToCaller(lines, variable, fromLine, toLine) {
|
|
40378
|
-
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${
|
|
41174
|
+
const varRe = new RegExp(`\\breturn\\b[^;]*\\b${escapeRegex3(variable)}\\b`);
|
|
40379
41175
|
for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
|
|
40380
41176
|
if (varRe.test(lines[l - 1] ?? "")) return true;
|
|
40381
41177
|
}
|
|
@@ -40388,7 +41184,7 @@ var ResourceLeakPass = class {
|
|
|
40388
41184
|
*/
|
|
40389
41185
|
fieldStoredName(lines, variable, openLine, toLine) {
|
|
40390
41186
|
const thisAssignRe = new RegExp(
|
|
40391
|
-
`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${
|
|
41187
|
+
`\\bthis\\s*\\.\\s*(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`
|
|
40392
41188
|
);
|
|
40393
41189
|
for (let l = openLine; l <= toLine && l <= lines.length; l++) {
|
|
40394
41190
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|
|
@@ -40463,7 +41259,7 @@ var ResourceLeakPass = class {
|
|
|
40463
41259
|
candidateField = variable;
|
|
40464
41260
|
} else {
|
|
40465
41261
|
const thisAssignRe = new RegExp(
|
|
40466
|
-
`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${
|
|
41262
|
+
`(?:\\bthis\\s*\\.\\s*)?(\\w+)\\s*=\\s*${escapeRegex3(variable)}\\b`
|
|
40467
41263
|
);
|
|
40468
41264
|
for (let l = fromLine; l <= toLine && l <= lines.length; l++) {
|
|
40469
41265
|
const m = thisAssignRe.exec(lines[l - 1] ?? "");
|