circle-ir 4.12.0 → 4.13.1

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