circle-ir 4.12.0 → 4.13.0

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