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.
Files changed (40) hide show
  1. package/README.md +9 -9
  2. package/dist/analysis/config-loader.d.ts.map +1 -1
  3. package/dist/analysis/config-loader.js +8 -2
  4. package/dist/analysis/config-loader.js.map +1 -1
  5. package/dist/analysis/constant-propagation/propagator.d.ts +2 -0
  6. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  7. package/dist/analysis/constant-propagation/propagator.js +56 -2
  8. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  9. package/dist/analysis/entry-point-detection.d.ts +7 -0
  10. package/dist/analysis/entry-point-detection.d.ts.map +1 -1
  11. package/dist/analysis/entry-point-detection.js +13 -1
  12. package/dist/analysis/entry-point-detection.js.map +1 -1
  13. package/dist/analysis/interprocedural.d.ts.map +1 -1
  14. package/dist/analysis/interprocedural.js +4 -0
  15. package/dist/analysis/interprocedural.js.map +1 -1
  16. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts +147 -0
  17. package/dist/analysis/passes/csharp-sanitizer-credit.d.ts.map +1 -0
  18. package/dist/analysis/passes/csharp-sanitizer-credit.js +564 -0
  19. package/dist/analysis/passes/csharp-sanitizer-credit.js.map +1 -0
  20. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  21. package/dist/analysis/passes/interprocedural-pass.js +89 -0
  22. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  23. package/dist/analysis/passes/language-sources-pass.d.ts +6 -1
  24. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  25. package/dist/analysis/passes/language-sources-pass.js +15 -2
  26. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  27. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  28. package/dist/analysis/passes/sink-filter-pass.js +49 -52
  29. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  30. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  31. package/dist/analysis/passes/taint-propagation-pass.js +348 -16
  32. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  33. package/dist/analysis/require-entry-path.js +50 -4
  34. package/dist/analysis/require-entry-path.js.map +1 -1
  35. package/dist/browser/circle-ir.js +861 -65
  36. package/dist/core/circle-ir-core.cjs +81 -4
  37. package/dist/core/circle-ir-core.js +81 -4
  38. package/dist/core/extractors/dfg.js +49 -0
  39. package/dist/core/extractors/dfg.js.map +1 -1
  40. 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
- { method: "format", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1] },
12978
- { method: "printf", class: "PrintWriter", type: "xss", cwe: "CWE-79", severity: "high", arg_positions: [0, 1] },
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
- const switchBlock = node.children.find((c) => c.type === "switch_block");
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
- return ref.replace(/<.*$/, "").trim();
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
- for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file)) {
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 mentions = (t) => {
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 (mentions(a[2])) guarded.add(a[1]);
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 (mentions(lt)) {
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 mentions = (t) => {
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 && mentions(a[2])) guarded.add(a[1]);
31352
- if (mentions(lines[l])) {
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 mentions = (t) => {
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 (mentions(a[2])) guarded.add(a[1]);
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 (mentions(lt)) {
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])${escapeRegex(v)}(?![\\w])`).test(rhs)
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
- for (const { re, type } of CSHARP_SANITIZER_RES) {
36167
- if (type === sink.type && re.test(sinkLineText)) return false;
36168
- }
36169
- const sanitizedVars = sanitizedByType.get(sink.type);
36170
- if (sanitizedVars && sanitizedVars.size > 0) {
36171
- for (const v of sanitizedVars) {
36172
- if (new RegExp(`(?<![\\w])${escapeRegex(v)}(?![\\w])`).test(sinkLineText)) return false;
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])${escapeRegex(v)}\\s*\\.`).test(text)) {
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
- const m = relativeLiteralArgRe.exec(text);
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${escapeRegex(name2)}\\s*\\(`)
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 escapeRegex(s) {
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 = escapeRegex(varName);
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 (reNaked.test(line) || reGuarded.test(line) || reSwitchCase.test(line)) return true;
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 ((language === "java" || language === "csharp") && typeof code === "string" && sourcesWithVar.length > 0) {
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 anchor = sourcesWithVar.reduce((a, s) => s.line < a.line ? s : a, sourcesWithVar[0]);
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
- if (known.has(obj)) continue;
39051
- if (!argsReferenceTaintedVar(rhs, sourcesWithVar)) continue;
39052
- sourcesWithVar.push({ ...anchor, variable: obj, line: i2 + 1 });
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
- if (!builders.has(obj) || known.has(obj)) continue;
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 escapeRegex2(s) {
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${escapeRegex2(variable)}\\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*${escapeRegex2(variable)}\\b`
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*${escapeRegex2(variable)}\\b`
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] ?? "");