circle-ir 3.129.0 → 3.132.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.
@@ -11732,8 +11732,10 @@ var DEFAULT_SINKS = [
11732
11732
  { method: "newTransformer", class: "TransformerFactory", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
11733
11733
  { method: "transform", class: "Transformer", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
11734
11734
  // SSRF - Server-Side Request Forgery (CWE-918) and Open Redirect (CWE-601)
11735
- { method: "sendRedirect", class: "HttpServletResponse", type: "ssrf", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11736
- { method: "sendRedirect", type: "ssrf", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11735
+ // Sprint 82 (#189): HttpServletResponse.sendRedirect is CWE-601 / open_redirect,
11736
+ // not ssrf. Re-typing so manifest sink_type='open_redirect' matches.
11737
+ { method: "sendRedirect", class: "HttpServletResponse", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11738
+ { method: "sendRedirect", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11737
11739
  { method: "openConnection", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
11738
11740
  { method: "openStream", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
11739
11741
  // NOTE: URL/URI constructors removed — constructing a URL object doesn't make a network
@@ -14611,12 +14613,19 @@ function canSourceReachSink(sourceType, sinkType) {
14611
14613
  // splitting (CWE-113) — Sprint 6, issue #86.
14612
14614
  // mass_assignment added to http_body / http_param: Object.assign(user, req.body),
14613
14615
  // User(**request.form) — CWE-915.
14614
- http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment"],
14615
- http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment"],
14616
- http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf"],
14617
- http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf"],
14618
- http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call"],
14619
- http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment"],
14616
+ // open_redirect added to http_param/http_query/http_header/http_cookie/http_body/http_path
14617
+ // Sprint 82 (#189): a user-controlled value reaching res.sendRedirect /
14618
+ // res.redirect / Location header / append_header(("Location", x)) /
14619
+ // Header().Set("Location", x) IS open_redirect (CWE-601). The reach map
14620
+ // previously omitted open_redirect so the inline-colocation flow detector
14621
+ // silently skipped all `http_* open_redirect` co-located flows
14622
+ // (java sendRedirect, JS res.redirect, Rust append_header tuple, etc.).
14623
+ http_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect"],
14624
+ http_body: ["sql_injection", "command_injection", "deserialization", "xxe", "xss", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect"],
14625
+ http_header: ["sql_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect"],
14626
+ http_cookie: ["sql_injection", "xss", "mybatis_mapper_call", "code_injection", "crlf", "open_redirect"],
14627
+ http_path: ["path_traversal", "sql_injection", "ssrf", "mybatis_mapper_call", "open_redirect"],
14628
+ http_query: ["sql_injection", "command_injection", "xss", "ssrf", "mybatis_mapper_call", "code_injection", "crlf", "mass_assignment", "open_redirect"],
14620
14629
  // ssrf added Sprint 57 #200: bash CGI/webhook handlers and scripts that
14621
14630
  // take a URL on stdin or as a positional CLI arg (`curl "$1"`,
14622
14631
  // `wget "$(read line)"`) and curl/wget it server-side are textbook SSRF
@@ -14630,8 +14639,8 @@ function canSourceReachSink(sourceType, sinkType) {
14630
14639
  network_input: ["sql_injection", "command_injection", "xss", "ssrf"],
14631
14640
  config_param: ["sql_injection", "command_injection", "path_traversal", "xss", "ssrf"],
14632
14641
  // Servlet init params
14633
- interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment"],
14634
- // Cross-method taint
14642
+ interprocedural_param: ["sql_injection", "command_injection", "path_traversal", "xss", "xpath_injection", "ldap_injection", "ssrf", "code_injection", "mybatis_mapper_call", "crlf", "mass_assignment", "open_redirect"],
14643
+ // Cross-method taint; Sprint 82 (#189) — open_redirect added
14635
14644
  plugin_param: ["sql_injection", "command_injection", "path_traversal", "xss", "code_injection"]
14636
14645
  // Plugin/config parameters
14637
14646
  };
@@ -23654,6 +23663,15 @@ var LanguageSourcesPass = class {
23654
23663
  for (const finding of goMisconfigFindings) {
23655
23664
  ctx.addFinding(finding);
23656
23665
  }
23666
+ for (const finding of findGoXssFindings(code, graph.ir.meta.file)) {
23667
+ ctx.addFinding(finding);
23668
+ }
23669
+ for (const finding of findGoLocationHeaderOpenRedirectFindings(
23670
+ code,
23671
+ graph.ir.meta.file
23672
+ )) {
23673
+ ctx.addFinding(finding);
23674
+ }
23657
23675
  }
23658
23676
  if (language === "python") {
23659
23677
  additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
@@ -23666,6 +23684,18 @@ var LanguageSourcesPass = class {
23666
23684
  for (const finding of pyMisconfigFindings) {
23667
23685
  ctx.addFinding(finding);
23668
23686
  }
23687
+ for (const finding of findPythonFlaskStringConcatXssFindings(code, graph.ir.meta.file)) {
23688
+ ctx.addFinding(finding);
23689
+ }
23690
+ for (const finding of findPythonJinjaMarkupXssFindings(code, graph.ir.meta.file)) {
23691
+ ctx.addFinding(finding);
23692
+ }
23693
+ for (const finding of findPythonHeadersSubscriptOpenRedirectFindings(
23694
+ code,
23695
+ graph.ir.meta.file
23696
+ )) {
23697
+ ctx.addFinding(finding);
23698
+ }
23669
23699
  }
23670
23700
  if (language === "rust") {
23671
23701
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
@@ -23687,6 +23717,12 @@ var LanguageSourcesPass = class {
23687
23717
  for (const finding of findRustWeakCryptoEcbFindings(code, graph.ir.meta.file)) {
23688
23718
  ctx.addFinding(finding);
23689
23719
  }
23720
+ for (const finding of findRustAppendHeaderTupleOpenRedirectFindings(
23721
+ code,
23722
+ graph.ir.meta.file
23723
+ )) {
23724
+ ctx.addFinding(finding);
23725
+ }
23690
23726
  }
23691
23727
  if (language === "javascript" || language === "typescript" || language === "htmljs") {
23692
23728
  additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
@@ -23694,9 +23730,23 @@ var LanguageSourcesPass = class {
23694
23730
  additionalSanitizers.push(...findJsCsvFormulaPrefixSanitizers(code));
23695
23731
  additionalSanitizers.push(...findJsWrapperFunctionSanitizers(code));
23696
23732
  additionalSanitizers.push(...findJsSsrfAllowlistGuardSanitizers(code));
23733
+ additionalSanitizers.push(...findJsArgvFormExecSanitizers(code));
23734
+ additionalSanitizers.push(...findJsParameterizedSqlSanitizers(code));
23697
23735
  for (const finding of findJsPatternFindings(code, graph.ir.meta.file)) {
23698
23736
  ctx.addFinding(finding);
23699
23737
  }
23738
+ for (const finding of findJsVueVHtmlXssFindings(code, graph.ir.meta.file)) {
23739
+ ctx.addFinding(finding);
23740
+ }
23741
+ for (const finding of findTsAngularBypassXssFindings(code, graph.ir.meta.file)) {
23742
+ ctx.addFinding(finding);
23743
+ }
23744
+ for (const finding of findJsDomOpenRedirectFindings(
23745
+ code,
23746
+ graph.ir.meta.file
23747
+ )) {
23748
+ ctx.addFinding(finding);
23749
+ }
23700
23750
  }
23701
23751
  if (language === "java") {
23702
23752
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
@@ -23708,6 +23758,9 @@ var LanguageSourcesPass = class {
23708
23758
  for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
23709
23759
  ctx.addFinding(finding);
23710
23760
  }
23761
+ for (const finding of findJavaResponseWriterXssFindings(code, graph.ir.meta.file)) {
23762
+ ctx.addFinding(finding);
23763
+ }
23711
23764
  }
23712
23765
  if (language === "python" || language === "javascript" || language === "typescript" || language === "go") {
23713
23766
  const exfilFindings = findExternalSecretExfiltrationFindings(
@@ -25454,6 +25507,55 @@ function findJsSsrfAllowlistGuardSanitizers(code) {
25454
25507
  }
25455
25508
  return sanitizers;
25456
25509
  }
25510
+ function findJsArgvFormExecSanitizers(code) {
25511
+ const sanitizers = [];
25512
+ const lines = code.split("\n");
25513
+ const argvExecRe = /\b(?:execFile|spawn)(?:Sync)?\s*\(\s*(?:'[^']*'|"[^"]*"|`[^`]*`)\s*,\s*\[/;
25514
+ const shellArgvRe = /\b(?:execFile|spawn)(?:Sync)?\s*\(\s*['"`](?:[\w./-]*\/)?(?:sh|bash|zsh|ksh|dash|cmd(?:\.exe)?|powershell|pwsh)['"`]\s*,\s*\[\s*['"`]-c['"`]/;
25515
+ for (let i2 = 0; i2 < lines.length; i2++) {
25516
+ const text = lines[i2];
25517
+ if (!argvExecRe.test(text)) continue;
25518
+ if (shellArgvRe.test(text)) continue;
25519
+ sanitizers.push({
25520
+ type: "js_argv_form_exec",
25521
+ method: "execFile",
25522
+ line: i2 + 1,
25523
+ sanitizes: ["command_injection", "external_taint_escape"]
25524
+ });
25525
+ }
25526
+ return sanitizers;
25527
+ }
25528
+ function findJsParameterizedSqlSanitizers(code) {
25529
+ const sanitizers = [];
25530
+ const lines = code.split("\n");
25531
+ const singleRe = /\.\s*(?:query|execute)\s*\(\s*'([^'\\]*(?:\\.[^'\\]*)*)'\s*,\s*\[/;
25532
+ const doubleRe = /\.\s*(?:query|execute)\s*\(\s*"([^"\\]*(?:\\.[^"\\]*)*)"\s*,\s*\[/;
25533
+ const tickRe = /\.\s*(?:query|execute)\s*\(\s*`([^`]*)`\s*,\s*\[/;
25534
+ const placeholderRe = /(?:\$\d+|\?)/;
25535
+ for (let i2 = 0; i2 < lines.length; i2++) {
25536
+ const text = lines[i2];
25537
+ let sql = null;
25538
+ const s = singleRe.exec(text);
25539
+ if (s) sql = s[1];
25540
+ if (sql == null) {
25541
+ const d = doubleRe.exec(text);
25542
+ if (d) sql = d[1];
25543
+ }
25544
+ if (sql == null) {
25545
+ const t = tickRe.exec(text);
25546
+ if (t && !/\$\{/.test(t[1])) sql = t[1];
25547
+ }
25548
+ if (sql == null) continue;
25549
+ if (!placeholderRe.test(sql)) continue;
25550
+ sanitizers.push({
25551
+ type: "js_parameterized_sql",
25552
+ method: "query",
25553
+ line: i2 + 1,
25554
+ sanitizes: ["sql_injection", "external_taint_escape"]
25555
+ });
25556
+ }
25557
+ return sanitizers;
25558
+ }
25457
25559
  function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
25458
25560
  const sanitizers = [];
25459
25561
  const lines = code.split("\n");
@@ -26212,6 +26314,612 @@ function findJsPatternFindings(code, file) {
26212
26314
  }
26213
26315
  return out2;
26214
26316
  }
26317
+ function findGoXssFindings(code, file) {
26318
+ const out2 = [];
26319
+ const lines = code.split("\n");
26320
+ const rwNames = /* @__PURE__ */ new Set();
26321
+ const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
26322
+ for (const line of lines) {
26323
+ let m;
26324
+ sigRe.lastIndex = 0;
26325
+ while ((m = sigRe.exec(line)) !== null) rwNames.add(m[1]);
26326
+ }
26327
+ if (rwNames.size === 0) return out2;
26328
+ const escaperRe = /\b(?:html|template)\.(?:EscapeString|HTMLEscapeString|HTMLEscaper|JSEscapeString|URLQueryEscaper)\s*\(/;
26329
+ for (let i2 = 0; i2 < lines.length; i2++) {
26330
+ const raw = lines[i2];
26331
+ const trimmed = raw.trim();
26332
+ if (!trimmed || trimmed.startsWith("//")) continue;
26333
+ const callRe = /\bfmt\.Fprint(?:f|ln)?\s*\(\s*([A-Za-z_]\w*)\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
26334
+ const m = trimmed.match(callRe);
26335
+ if (!m) continue;
26336
+ if (!rwNames.has(m[1])) continue;
26337
+ const tail = m[2];
26338
+ if (escaperRe.test(tail)) continue;
26339
+ if (!/[A-Za-z_]\w*/.test(tail.replace(/"[^"]*"|`[^`]*`/g, ""))) continue;
26340
+ out2.push({
26341
+ id: `xss-${file}-${i2 + 1}`,
26342
+ pass: "language-sources",
26343
+ category: "security",
26344
+ rule_id: "xss",
26345
+ cwe: "CWE-79",
26346
+ severity: "high",
26347
+ level: "error",
26348
+ message: "Reflected XSS: fmt.Fprint writes data to http.ResponseWriter without HTML escaping. Wrap user-controlled args with html.EscapeString / template.HTMLEscapeString.",
26349
+ file,
26350
+ line: i2 + 1,
26351
+ snippet: trimmed.substring(0, 100)
26352
+ });
26353
+ }
26354
+ return out2;
26355
+ }
26356
+ function findJavaResponseWriterXssFindings(code, file) {
26357
+ const out2 = [];
26358
+ const lines = code.split("\n");
26359
+ const respNames = /* @__PURE__ */ new Set();
26360
+ const sigRe = /\bHttpServletResponse\s+([A-Za-z_]\w*)\b/g;
26361
+ for (const line of lines) {
26362
+ let m;
26363
+ sigRe.lastIndex = 0;
26364
+ while ((m = sigRe.exec(line)) !== null) respNames.add(m[1]);
26365
+ }
26366
+ if (respNames.size === 0) return out2;
26367
+ const safeWrapRe = /\b(?:Encode\.(?:forHtml|forHtmlAttribute|forHtmlContent|forJavaScript)|StringEscapeUtils\.escape(?:Html3|Html4|EcmaScript|Xml)|HtmlUtils\.htmlEscape(?:Decimal|Hex)?|Escaper\.escapeHtml|HtmlEscapers\.(?:escapeHtml|htmlEscaper)|Encoder\.encodeForHTML(?:Attribute)?|Jsoup\.clean)\s*\(/;
26368
+ for (let i2 = 0; i2 < lines.length; i2++) {
26369
+ const raw = lines[i2];
26370
+ const trimmed = raw.trim();
26371
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
26372
+ const chainRe = /\b([A-Za-z_]\w*)\s*\.\s*getWriter\s*\(\s*\)\s*\.\s*(print|println|write|printf|format|append)\s*\(([\s\S]*)\)\s*;?\s*(?:\/\/.*)?$/;
26373
+ const m = trimmed.match(chainRe);
26374
+ if (!m) continue;
26375
+ const recv = m[1];
26376
+ if (!respNames.has(recv)) continue;
26377
+ const args2 = m[3];
26378
+ if (safeWrapRe.test(args2)) continue;
26379
+ const argsTrim = args2.trim();
26380
+ if (/^"(?:\\.|[^"\\])*"$/.test(argsTrim)) continue;
26381
+ if (!/[A-Za-z_]\w*/.test(argsTrim)) continue;
26382
+ out2.push({
26383
+ id: `xss-${file}-${i2 + 1}`,
26384
+ pass: "language-sources",
26385
+ category: "security",
26386
+ rule_id: "xss",
26387
+ cwe: "CWE-79",
26388
+ severity: "high",
26389
+ level: "error",
26390
+ message: "Reflected XSS: HttpServletResponse.getWriter()." + m[2] + "(...) writes data to the response without HTML escaping. Wrap user-controlled values with OWASP Encode.forHtml / StringEscapeUtils.escapeHtml4 / Jsoup.clean.",
26391
+ file,
26392
+ line: i2 + 1,
26393
+ snippet: trimmed.substring(0, 100)
26394
+ });
26395
+ }
26396
+ return out2;
26397
+ }
26398
+ function findJsVueVHtmlXssFindings(code, file) {
26399
+ const out2 = [];
26400
+ const lines = code.split("\n");
26401
+ const sourceRe = /\b(?:URLSearchParams|location\s*\.\s*(?:search|hash|href|pathname)|window\s*\.\s*location|route\s*\.\s*(?:query|params)|router\s*\.\s*(?:currentRoute|query)|\$route\s*\.\s*(?:query|params)|fetch\s*\(|axios\s*\.\s*(?:get|post)|XMLHttpRequest|document\s*\.\s*location)\b/;
26402
+ const tplRe = /\btemplate\s*:\s*(['"`])([\s\S]*?)\1/g;
26403
+ let tm;
26404
+ const boundVars = /* @__PURE__ */ new Set();
26405
+ while ((tm = tplRe.exec(code)) !== null) {
26406
+ const tpl = tm[2];
26407
+ const vhRe = /\bv-html\s*=\s*"([^"]+)"/g;
26408
+ let vm;
26409
+ while ((vm = vhRe.exec(tpl)) !== null) {
26410
+ const expr = vm[1].trim();
26411
+ const idMatch = expr.match(/^([A-Za-z_$][\w$]*)/);
26412
+ if (idMatch) boundVars.add(idMatch[1]);
26413
+ }
26414
+ }
26415
+ if (boundVars.size === 0) return out2;
26416
+ const taintedSet = /* @__PURE__ */ new Set();
26417
+ const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
26418
+ let am;
26419
+ assignRe.lastIndex = 0;
26420
+ while ((am = assignRe.exec(code)) !== null) {
26421
+ if (sourceRe.test(am[2])) taintedSet.add(am[1]);
26422
+ }
26423
+ for (let pass = 0; pass < 3; pass++) {
26424
+ assignRe.lastIndex = 0;
26425
+ const before = taintedSet.size;
26426
+ while ((am = assignRe.exec(code)) !== null) {
26427
+ if (taintedSet.has(am[1])) continue;
26428
+ for (const tv of taintedSet) {
26429
+ if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
26430
+ taintedSet.add(am[1]);
26431
+ break;
26432
+ }
26433
+ }
26434
+ }
26435
+ if (taintedSet.size === before) break;
26436
+ }
26437
+ const taintedBindings = /* @__PURE__ */ new Set();
26438
+ for (const v of boundVars) {
26439
+ const bindRe = new RegExp(`\\b${v}\\s*[:=]\\s*([^,;\\n]+)`);
26440
+ for (const line of lines) {
26441
+ const m = line.match(bindRe);
26442
+ if (!m) continue;
26443
+ const val = m[1];
26444
+ if (sourceRe.test(val)) {
26445
+ taintedBindings.add(v);
26446
+ break;
26447
+ }
26448
+ let found = false;
26449
+ for (const tv of taintedSet) {
26450
+ if (new RegExp(`\\b${tv}\\b`).test(val)) {
26451
+ taintedBindings.add(v);
26452
+ found = true;
26453
+ break;
26454
+ }
26455
+ }
26456
+ if (found) break;
26457
+ }
26458
+ const propsRe = new RegExp(
26459
+ `\\bprops\\s*:\\s*\\[[^\\]]*['"\`]${v}['"\`][^\\]]*\\]`
26460
+ );
26461
+ if (propsRe.test(code)) taintedBindings.add(v);
26462
+ }
26463
+ if (taintedBindings.size === 0) return out2;
26464
+ for (let i2 = 0; i2 < lines.length; i2++) {
26465
+ const raw = lines[i2];
26466
+ if (!/v-html\s*=/.test(raw)) continue;
26467
+ const vm = raw.match(/v-html\s*=\s*"([^"]+)"/);
26468
+ if (!vm) continue;
26469
+ const idMatch = vm[1].trim().match(/^([A-Za-z_$][\w$]*)/);
26470
+ if (!idMatch || !taintedBindings.has(idMatch[1])) continue;
26471
+ out2.push({
26472
+ id: `xss-${file}-${i2 + 1}`,
26473
+ pass: "language-sources",
26474
+ category: "security",
26475
+ rule_id: "xss",
26476
+ cwe: "CWE-79",
26477
+ severity: "high",
26478
+ level: "error",
26479
+ message: 'Vue v-html XSS: directive binds to "' + idMatch[1] + '" which is sourced from user-controlled input. Use {{ }} interpolation (auto-escapes) or sanitize the HTML with DOMPurify before binding.',
26480
+ file,
26481
+ line: i2 + 1,
26482
+ snippet: raw.trim().substring(0, 100)
26483
+ });
26484
+ }
26485
+ return out2;
26486
+ }
26487
+ function findTsAngularBypassXssFindings(code, file) {
26488
+ const out2 = [];
26489
+ const lines = code.split("\n");
26490
+ const sanitizerNames = /* @__PURE__ */ new Set();
26491
+ const declRe = /\b(?:public|private|protected|readonly|\s)?\s*([A-Za-z_$][\w$]*)\s*:\s*DomSanitizer\b/g;
26492
+ let m;
26493
+ declRe.lastIndex = 0;
26494
+ while ((m = declRe.exec(code)) !== null) sanitizerNames.add(m[1]);
26495
+ if (sanitizerNames.size === 0) return out2;
26496
+ const assignRe = /\bthis\s*\.\s*([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\b/g;
26497
+ let am;
26498
+ assignRe.lastIndex = 0;
26499
+ while ((am = assignRe.exec(code)) !== null) {
26500
+ if (sanitizerNames.has(am[2])) sanitizerNames.add(am[1]);
26501
+ }
26502
+ for (let i2 = 0; i2 < lines.length; i2++) {
26503
+ const raw = lines[i2];
26504
+ const trimmed = raw.trim();
26505
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
26506
+ const callRe = /\b(?:this\s*\.\s*)?([A-Za-z_$][\w$]*)\s*\.\s*bypassSecurityTrust(Html|Script|Url|ResourceUrl|Style)\s*\(([\s\S]*?)\)/;
26507
+ const cm = trimmed.match(callRe);
26508
+ if (!cm) continue;
26509
+ const recv = cm[1];
26510
+ if (!sanitizerNames.has(recv)) continue;
26511
+ const arg = cm[3].trim();
26512
+ if (/^(['"`])[^'"`]*\1$/.test(arg)) continue;
26513
+ out2.push({
26514
+ id: `xss-${file}-${i2 + 1}`,
26515
+ pass: "language-sources",
26516
+ category: "security",
26517
+ rule_id: "xss",
26518
+ cwe: "CWE-79",
26519
+ severity: "high",
26520
+ level: "error",
26521
+ message: "Angular XSS: DomSanitizer.bypassSecurityTrust" + cm[2] + "() opts out of Angular's built-in sanitization for a non-literal argument. Prefer leaving Angular's default sanitizer in place, or sanitize the input with DOMPurify before bypassing.",
26522
+ file,
26523
+ line: i2 + 1,
26524
+ snippet: trimmed.substring(0, 100)
26525
+ });
26526
+ }
26527
+ return out2;
26528
+ }
26529
+ function findPythonFlaskStringConcatXssFindings(code, file) {
26530
+ const out2 = [];
26531
+ const lines = code.split("\n");
26532
+ const fns = [];
26533
+ const defRe = /^(\s*)def\s+([A-Za-z_]\w*)\s*\(/;
26534
+ for (let i2 = 0; i2 < lines.length; i2++) {
26535
+ const raw = lines[i2];
26536
+ const dm = raw.match(defRe);
26537
+ if (!dm) continue;
26538
+ const indent = dm[1].length;
26539
+ let hasRoute = false;
26540
+ for (let j = i2 - 1; j >= 0; j--) {
26541
+ const prev = lines[j].trim();
26542
+ if (prev === "" || prev.startsWith("#")) continue;
26543
+ if (!prev.startsWith("@")) break;
26544
+ if (/@\s*[A-Za-z_]\w*\s*\.\s*route\s*\(/.test(prev) || /@\s*route\s*\(/.test(prev)) {
26545
+ hasRoute = true;
26546
+ }
26547
+ }
26548
+ if (!hasRoute) continue;
26549
+ let end = lines.length;
26550
+ for (let k = i2 + 1; k < lines.length; k++) {
26551
+ const ln = lines[k];
26552
+ if (!ln.trim()) continue;
26553
+ const ind = ln.match(/^(\s*)/)?.[1].length ?? 0;
26554
+ if (ind <= indent) {
26555
+ end = k;
26556
+ break;
26557
+ }
26558
+ }
26559
+ fns.push({ name: dm[2], startLine: i2 + 1, endLine: end });
26560
+ }
26561
+ if (fns.length === 0) return out2;
26562
+ const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
26563
+ const escapeWrapRe = /\b(?:html\.escape|markupsafe\.escape|escape|markupsafe\.Markup\s*\.\s*escape|werkzeug\.utils\.escape)\s*\(/;
26564
+ for (const fn of fns) {
26565
+ const taintedVars = /* @__PURE__ */ new Set();
26566
+ for (let li = fn.startLine; li < fn.endLine; li++) {
26567
+ const raw = lines[li];
26568
+ const rm = raw.match(requestSourceRe);
26569
+ if (rm) taintedVars.add(rm[1]);
26570
+ }
26571
+ for (let li = fn.startLine; li < fn.endLine; li++) {
26572
+ const raw = lines[li];
26573
+ const trimmed = raw.trim();
26574
+ if (!trimmed || trimmed.startsWith("#")) continue;
26575
+ const retMatch = trimmed.match(/^(?:return|yield)\s+(.+)$/);
26576
+ if (!retMatch) continue;
26577
+ const expr = retMatch[1];
26578
+ const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/.test(
26579
+ expr
26580
+ );
26581
+ const hasTaintedVar = [...taintedVars].some(
26582
+ (v) => new RegExp(`\\b${v}\\b`).test(expr)
26583
+ );
26584
+ if (!hasTaintedVar && !inlineSource) continue;
26585
+ if (escapeWrapRe.test(expr)) continue;
26586
+ const buildsHtml = /['"`]\s*\+/.test(expr) || /\+\s*['"`]/.test(expr) || // f-string with interpolation. Allow any non-`{` char between the
26587
+ // opening quote and the first `{` (Python f-strings may embed `"`
26588
+ // when single-quoted and vice-versa, e.g. f'<a href="{u}">').
26589
+ /\bf['"`][^{\n]*\{[^}]+\}/.test(expr) || /['"`]\s*%\s*[^=]/.test(expr) || /['"`]\.\s*format\s*\(/.test(expr);
26590
+ if (!buildsHtml) continue;
26591
+ out2.push({
26592
+ id: `xss-${file}-${li + 1}`,
26593
+ pass: "language-sources",
26594
+ category: "security",
26595
+ rule_id: "xss",
26596
+ cwe: "CWE-79",
26597
+ severity: "high",
26598
+ level: "error",
26599
+ message: "Reflected XSS: Flask route returns HTML built from request input via string concatenation / f-string / format. Wrap user input with markupsafe.escape() or render via a Jinja2 template (autoescape on by default).",
26600
+ file,
26601
+ line: li + 1,
26602
+ snippet: trimmed.substring(0, 100)
26603
+ });
26604
+ break;
26605
+ }
26606
+ }
26607
+ return out2;
26608
+ }
26609
+ function findPythonJinjaMarkupXssFindings(code, file) {
26610
+ const out2 = [];
26611
+ const lines = code.split("\n");
26612
+ const importRe = /^\s*from\s+(?:markupsafe|flask)\s+import\s+(?:[^#\n]*\b)?Markup\b/m;
26613
+ const classDefRe = /^\s*class\s+Markup\b/m;
26614
+ if (!importRe.test(code)) return out2;
26615
+ if (classDefRe.test(code)) return out2;
26616
+ const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
26617
+ const taintedVars = /* @__PURE__ */ new Set();
26618
+ for (const line of lines) {
26619
+ const m = line.match(requestSourceRe);
26620
+ if (m) taintedVars.add(m[1]);
26621
+ }
26622
+ if (taintedVars.size === 0) return out2;
26623
+ if (!/\.\s*render\s*\(/.test(code)) return out2;
26624
+ const markupCallRe = /\bMarkup\s*\(\s*([A-Za-z_]\w*)\s*[,)]/g;
26625
+ for (let i2 = 0; i2 < lines.length; i2++) {
26626
+ const raw = lines[i2];
26627
+ const trimmed = raw.trim();
26628
+ if (!trimmed || trimmed.startsWith("#")) continue;
26629
+ let mm;
26630
+ markupCallRe.lastIndex = 0;
26631
+ while ((mm = markupCallRe.exec(trimmed)) !== null) {
26632
+ const argName = mm[1];
26633
+ if (!taintedVars.has(argName)) continue;
26634
+ out2.push({
26635
+ id: `xss-${file}-${i2 + 1}`,
26636
+ pass: "language-sources",
26637
+ category: "security",
26638
+ rule_id: "xss",
26639
+ cwe: "CWE-79",
26640
+ severity: "high",
26641
+ level: "error",
26642
+ message: "Jinja2 autoescape bypass: Markup(" + argName + ") wraps a request-derived value, disabling autoescape when rendered. Pass the raw value to render() and let Jinja2 auto-escape, or sanitize with bleach.clean first.",
26643
+ file,
26644
+ line: i2 + 1,
26645
+ snippet: trimmed.substring(0, 100)
26646
+ });
26647
+ break;
26648
+ }
26649
+ }
26650
+ return out2;
26651
+ }
26652
+ function findGoLocationHeaderOpenRedirectFindings(code, file) {
26653
+ const out2 = [];
26654
+ const lines = code.split("\n");
26655
+ const rwNames = /* @__PURE__ */ new Set();
26656
+ const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
26657
+ for (const line of lines) {
26658
+ let m;
26659
+ sigRe.lastIndex = 0;
26660
+ while ((m = sigRe.exec(line)) !== null) rwNames.add(m[1]);
26661
+ }
26662
+ if (rwNames.size === 0) return out2;
26663
+ const safeWrapRe = /\b(?:net\/url|url)\.Parse\b[\s\S]{0,120}?\b(?:IsAbs|Host)\b|\bstrings\.HasPrefix\s*\(/;
26664
+ const callRe = /\b([A-Za-z_]\w*)\s*\.\s*Header\s*\(\s*\)\s*\.\s*(?:Set|Add)\s*\(\s*(['"])([^'"]+)\2\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
26665
+ for (let i2 = 0; i2 < lines.length; i2++) {
26666
+ const raw = lines[i2];
26667
+ const trimmed = raw.trim();
26668
+ if (!trimmed || trimmed.startsWith("//")) continue;
26669
+ const m = trimmed.match(callRe);
26670
+ if (!m) continue;
26671
+ const recv = m[1];
26672
+ if (!rwNames.has(recv)) continue;
26673
+ const key = m[3];
26674
+ if (key.toLowerCase() !== "location") continue;
26675
+ const valExpr = m[4];
26676
+ const valTrim = valExpr.trim();
26677
+ if (/^"(?:\\.|[^"\\])*"$/.test(valTrim)) continue;
26678
+ if (safeWrapRe.test(valExpr)) continue;
26679
+ out2.push({
26680
+ id: `open_redirect-${file}-${i2 + 1}`,
26681
+ pass: "language-sources",
26682
+ category: "security",
26683
+ rule_id: "open_redirect",
26684
+ cwe: "CWE-601",
26685
+ severity: "high",
26686
+ level: "error",
26687
+ message: 'Open redirect: ResponseWriter.Header().Set("Location", ...) writes a user-controlled value into the Location header without validating the target. Restrict to an allow-list of hosts/paths or compare against a known-safe set before issuing the redirect.',
26688
+ file,
26689
+ line: i2 + 1,
26690
+ snippet: trimmed.substring(0, 100)
26691
+ });
26692
+ }
26693
+ return out2;
26694
+ }
26695
+ function findPythonHeadersSubscriptOpenRedirectFindings(code, file) {
26696
+ const out2 = [];
26697
+ const lines = code.split("\n");
26698
+ const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
26699
+ const taintedVars = /* @__PURE__ */ new Set();
26700
+ for (const line of lines) {
26701
+ const m = line.match(requestSourceRe);
26702
+ if (m) taintedVars.add(m[1]);
26703
+ }
26704
+ const safeWrapRe = /\b(?:urllib\.parse\.urlparse|urlparse)\s*\([\s\S]{0,120}?\bnetloc\b|\b(?:startswith)\s*\(/;
26705
+ const subscriptRe = /\b([A-Za-z_]\w*)\s*\.\s*headers\s*\[\s*(['"])([^'"]+)\2\s*\]\s*=\s*(.+)$/;
26706
+ for (let i2 = 0; i2 < lines.length; i2++) {
26707
+ const raw = lines[i2];
26708
+ const trimmed = raw.trim();
26709
+ if (!trimmed || trimmed.startsWith("#")) continue;
26710
+ const m = trimmed.match(subscriptRe);
26711
+ if (!m) continue;
26712
+ const key = m[3];
26713
+ if (key.toLowerCase() !== "location") continue;
26714
+ const rhs = m[4].replace(/\s*(?:#.*)?$/, "").trim();
26715
+ if (/^['"](?:\\.|[^'"\\])*['"]$/.test(rhs)) continue;
26716
+ if (safeWrapRe.test(rhs)) continue;
26717
+ const hasTaintedVar = [...taintedVars].some(
26718
+ (v) => new RegExp(`\\b${v}\\b`).test(rhs)
26719
+ );
26720
+ const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers|json)(?:\s*\.\s*get\s*\(|\s*\[)/.test(
26721
+ rhs
26722
+ );
26723
+ if (!hasTaintedVar && !inlineSource) continue;
26724
+ out2.push({
26725
+ id: `open_redirect-${file}-${i2 + 1}`,
26726
+ pass: "language-sources",
26727
+ category: "security",
26728
+ rule_id: "open_redirect",
26729
+ cwe: "CWE-601",
26730
+ severity: "high",
26731
+ level: "error",
26732
+ message: 'Open redirect: response.headers["Location"] = ... assigns a user-controlled value to the Location header. Validate the URL against an allow-list of trusted hosts before sending.',
26733
+ file,
26734
+ line: i2 + 1,
26735
+ snippet: trimmed.substring(0, 100)
26736
+ });
26737
+ }
26738
+ return out2;
26739
+ }
26740
+ function findRustAppendHeaderTupleOpenRedirectFindings(code, file) {
26741
+ const out2 = [];
26742
+ const lines = code.split("\n");
26743
+ const extractorRe = /\b([A-Za-z_]\w*)\s*:\s*(?:web\s*::\s*)?(?:Query|Path|Form|Json)\s*<|\b([A-Za-z_]\w*)\s*:\s*&?\s*HttpRequest\b/g;
26744
+ const extractorParams = /* @__PURE__ */ new Set();
26745
+ for (const line of lines) {
26746
+ let m;
26747
+ extractorRe.lastIndex = 0;
26748
+ while ((m = extractorRe.exec(line)) !== null) {
26749
+ const name2 = m[1] ?? m[2];
26750
+ if (name2) extractorParams.add(name2);
26751
+ }
26752
+ }
26753
+ if (extractorParams.size === 0) return out2;
26754
+ const taintedVars = new Set(extractorParams);
26755
+ const letRe = /\blet\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::[^=]+)?=\s*([^;]+);/g;
26756
+ for (let pass = 0; pass < 4; pass++) {
26757
+ const before = taintedVars.size;
26758
+ let lm;
26759
+ letRe.lastIndex = 0;
26760
+ while ((lm = letRe.exec(code)) !== null) {
26761
+ const name2 = lm[1];
26762
+ const rhs = lm[2];
26763
+ if (taintedVars.has(name2)) continue;
26764
+ for (const tv of taintedVars) {
26765
+ if (new RegExp(`\\b${tv}\\b`).test(rhs)) {
26766
+ taintedVars.add(name2);
26767
+ break;
26768
+ }
26769
+ }
26770
+ }
26771
+ if (taintedVars.size === before) break;
26772
+ }
26773
+ const safeWrapRe = /\b(?:Url\s*::\s*parse|url\s*::\s*Url\s*::\s*parse)\s*\([\s\S]{0,160}?\b(?:host_str|host|origin)\b/;
26774
+ const tupleCallRe = /\.\s*(?:append_header|insert_header)\s*\(\s*\(\s*(['"])([^'"]+)\1\s*,\s*([^)]*?)\s*\)\s*\)/g;
26775
+ for (let i2 = 0; i2 < lines.length; i2++) {
26776
+ const raw = lines[i2];
26777
+ const trimmed = raw.trim();
26778
+ if (!trimmed || trimmed.startsWith("//")) continue;
26779
+ let cm;
26780
+ tupleCallRe.lastIndex = 0;
26781
+ while ((cm = tupleCallRe.exec(raw)) !== null) {
26782
+ const key = cm[2];
26783
+ if (key.toLowerCase() !== "location") continue;
26784
+ const valExpr = cm[3];
26785
+ if (/^['"](?:\\.|[^'"\\])*['"]$/.test(valExpr.trim())) continue;
26786
+ if (safeWrapRe.test(valExpr)) continue;
26787
+ const hasTainted = [...taintedVars].some(
26788
+ (v) => new RegExp(`\\b${v}\\b`).test(valExpr)
26789
+ );
26790
+ if (!hasTainted) continue;
26791
+ out2.push({
26792
+ id: `open_redirect-${file}-${i2 + 1}`,
26793
+ pass: "language-sources",
26794
+ category: "security",
26795
+ rule_id: "open_redirect",
26796
+ cwe: "CWE-601",
26797
+ severity: "high",
26798
+ level: "error",
26799
+ message: 'Open redirect: HttpResponse builder .append_header(("Location", ...)) sets the Location header from a request-derived value without allow-list validation. Parse with url::Url::parse and compare host_str against an allow-list.',
26800
+ file,
26801
+ line: i2 + 1,
26802
+ snippet: trimmed.substring(0, 100)
26803
+ });
26804
+ break;
26805
+ }
26806
+ }
26807
+ return out2;
26808
+ }
26809
+ function findJsDomOpenRedirectFindings(code, file) {
26810
+ const out2 = [];
26811
+ const lines = code.split("\n");
26812
+ const domSourceRe = /\blocation\s*\.\s*(?:search|hash|href|pathname)\b|\bwindow\s*\.\s*location\s*\.\s*(?:search|hash|href|pathname)\b|\bURLSearchParams\b|\bdocument\s*\.\s*(?:referrer|URL|location\s*\.\s*(?:search|hash|href|pathname))\b|\bwindow\s*\.\s*name\b/;
26813
+ const taintedVars = /* @__PURE__ */ new Set();
26814
+ const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
26815
+ let am;
26816
+ assignRe.lastIndex = 0;
26817
+ while ((am = assignRe.exec(code)) !== null) {
26818
+ if (domSourceRe.test(am[2])) taintedVars.add(am[1]);
26819
+ }
26820
+ for (let pass = 0; pass < 4; pass++) {
26821
+ const before = taintedVars.size;
26822
+ assignRe.lastIndex = 0;
26823
+ while ((am = assignRe.exec(code)) !== null) {
26824
+ if (taintedVars.has(am[1])) continue;
26825
+ for (const tv of taintedVars) {
26826
+ if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
26827
+ taintedVars.add(am[1]);
26828
+ break;
26829
+ }
26830
+ }
26831
+ }
26832
+ if (taintedVars.size === before) break;
26833
+ }
26834
+ const safeWrapRe = /\b(?:startsWith|includes)\s*\(\s*['"`]\/[^'"`]*['"`]\s*\)|\bnew\s+URL\s*\([\s\S]{0,80}?\)\s*\.\s*(?:origin|hostname)\s*===/;
26835
+ const containsTaint = (expr) => {
26836
+ if (domSourceRe.test(expr)) return true;
26837
+ for (const tv of taintedVars) {
26838
+ if (new RegExp(`\\b${tv}\\b`).test(expr)) return true;
26839
+ }
26840
+ return false;
26841
+ };
26842
+ const hrefSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*href\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
26843
+ const locSinkRe = /\b(?:window|document|self|top|parent)\s*\.\s*location\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
26844
+ const contentSinkRe = /\.\s*content\s*=\s*([^\n]+?)\s*$/;
26845
+ const callSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*(?:assign|replace)\s*\(\s*([^)]+)\)/;
26846
+ const emit = (line, msg, snippet) => {
26847
+ out2.push({
26848
+ id: `open_redirect-${file}-${line}`,
26849
+ pass: "language-sources",
26850
+ category: "security",
26851
+ rule_id: "open_redirect",
26852
+ cwe: "CWE-601",
26853
+ severity: "high",
26854
+ level: "error",
26855
+ message: msg,
26856
+ file,
26857
+ line,
26858
+ snippet: snippet.substring(0, 100)
26859
+ });
26860
+ };
26861
+ const isLiteralOnly = (expr) => /^['"`](?:\\.|[^'"`\\])*['"`]$/.test(expr.trim());
26862
+ for (let i2 = 0; i2 < lines.length; i2++) {
26863
+ const raw = lines[i2];
26864
+ const trimmed = raw.trim();
26865
+ if (!trimmed || trimmed.startsWith("//")) continue;
26866
+ let matched = false;
26867
+ const hm = trimmed.match(hrefSinkRe);
26868
+ if (hm) {
26869
+ const rhs = hm[1].trim();
26870
+ if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
26871
+ emit(
26872
+ i2 + 1,
26873
+ "Open redirect: assignment to location.href uses a value derived from a URL query/hash without allow-list validation. Compare the target origin to a known-safe set before navigating.",
26874
+ trimmed
26875
+ );
26876
+ matched = true;
26877
+ }
26878
+ }
26879
+ if (!matched) {
26880
+ const lm = trimmed.match(locSinkRe);
26881
+ if (lm) {
26882
+ const rhs = lm[1].trim();
26883
+ if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
26884
+ emit(
26885
+ i2 + 1,
26886
+ "Open redirect: assignment to window.location uses a value derived from a URL query/hash without allow-list validation. Compare the target origin to a known-safe set before navigating.",
26887
+ trimmed
26888
+ );
26889
+ matched = true;
26890
+ }
26891
+ }
26892
+ }
26893
+ if (!matched) {
26894
+ const cm = trimmed.match(contentSinkRe);
26895
+ if (cm) {
26896
+ const rhs = cm[1].replace(/\s*\/\/.*$/, "").replace(/\s*;\s*$/, "").trim();
26897
+ if (!isLiteralOnly(rhs) && /['"`][^'"`]*\burl\s*=/i.test(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
26898
+ emit(
26899
+ i2 + 1,
26900
+ "Open redirect: DOM assignment to <meta>.content with a meta-refresh URL built from a URL query/hash without allow-list validation. Validate origin before navigating.",
26901
+ trimmed
26902
+ );
26903
+ matched = true;
26904
+ }
26905
+ }
26906
+ }
26907
+ if (!matched) {
26908
+ const callm = trimmed.match(callSinkRe);
26909
+ if (callm) {
26910
+ const arg = callm[1].trim();
26911
+ if (!isLiteralOnly(arg) && containsTaint(arg) && !safeWrapRe.test(arg)) {
26912
+ emit(
26913
+ i2 + 1,
26914
+ "Open redirect: location.assign / location.replace invoked with a value derived from a URL query/hash without allow-list validation. Validate origin before navigating.",
26915
+ trimmed
26916
+ );
26917
+ }
26918
+ }
26919
+ }
26920
+ }
26921
+ return out2;
26922
+ }
26215
26923
 
26216
26924
  // src/analysis/passes/sink-filter-pass.ts
26217
26925
  var JS_XSS_SANITIZERS = [
@@ -27546,13 +28254,13 @@ var TaintPropagationPass = class {
27546
28254
  }));
27547
28255
  const arrayFlows = detectArrayElementFlows(calls, sources, sinks, constProp.taintedArrayElements, constProp.unreachableLines, types) ?? [];
27548
28256
  for (const f of arrayFlows) {
27549
- if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
28257
+ if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
27550
28258
  flows.push(f);
27551
28259
  }
27552
28260
  }
27553
28261
  const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
27554
28262
  for (const f of collectionFlows) {
27555
- if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) continue;
28263
+ if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) continue;
27556
28264
  const flowForCheck = {
27557
28265
  source: { line: f.source_line },
27558
28266
  sink: { line: f.sink_line },
@@ -27571,7 +28279,7 @@ var TaintPropagationPass = class {
27571
28279
  }
27572
28280
  const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
27573
28281
  for (const f of paramFlows) {
27574
- if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
28282
+ if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
27575
28283
  flows.push(f);
27576
28284
  }
27577
28285
  }
@@ -27961,7 +28669,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
27961
28669
  }
27962
28670
  const paramSource = methodParamSources.get(arg.variable);
27963
28671
  if (paramSource) {
27964
- const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
28672
+ const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
27965
28673
  if (!exists) {
27966
28674
  if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
27967
28675
  continue;