circle-ir 3.131.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
  };
@@ -23657,6 +23666,12 @@ var LanguageSourcesPass = class {
23657
23666
  for (const finding of findGoXssFindings(code, graph.ir.meta.file)) {
23658
23667
  ctx.addFinding(finding);
23659
23668
  }
23669
+ for (const finding of findGoLocationHeaderOpenRedirectFindings(
23670
+ code,
23671
+ graph.ir.meta.file
23672
+ )) {
23673
+ ctx.addFinding(finding);
23674
+ }
23660
23675
  }
23661
23676
  if (language === "python") {
23662
23677
  additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
@@ -23675,6 +23690,12 @@ var LanguageSourcesPass = class {
23675
23690
  for (const finding of findPythonJinjaMarkupXssFindings(code, graph.ir.meta.file)) {
23676
23691
  ctx.addFinding(finding);
23677
23692
  }
23693
+ for (const finding of findPythonHeadersSubscriptOpenRedirectFindings(
23694
+ code,
23695
+ graph.ir.meta.file
23696
+ )) {
23697
+ ctx.addFinding(finding);
23698
+ }
23678
23699
  }
23679
23700
  if (language === "rust") {
23680
23701
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
@@ -23696,6 +23717,12 @@ var LanguageSourcesPass = class {
23696
23717
  for (const finding of findRustWeakCryptoEcbFindings(code, graph.ir.meta.file)) {
23697
23718
  ctx.addFinding(finding);
23698
23719
  }
23720
+ for (const finding of findRustAppendHeaderTupleOpenRedirectFindings(
23721
+ code,
23722
+ graph.ir.meta.file
23723
+ )) {
23724
+ ctx.addFinding(finding);
23725
+ }
23699
23726
  }
23700
23727
  if (language === "javascript" || language === "typescript" || language === "htmljs") {
23701
23728
  additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
@@ -23714,6 +23741,12 @@ var LanguageSourcesPass = class {
23714
23741
  for (const finding of findTsAngularBypassXssFindings(code, graph.ir.meta.file)) {
23715
23742
  ctx.addFinding(finding);
23716
23743
  }
23744
+ for (const finding of findJsDomOpenRedirectFindings(
23745
+ code,
23746
+ graph.ir.meta.file
23747
+ )) {
23748
+ ctx.addFinding(finding);
23749
+ }
23717
23750
  }
23718
23751
  if (language === "java") {
23719
23752
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
@@ -26616,6 +26649,277 @@ function findPythonJinjaMarkupXssFindings(code, file) {
26616
26649
  }
26617
26650
  return out2;
26618
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
+ }
26619
26923
 
26620
26924
  // src/analysis/passes/sink-filter-pass.ts
26621
26925
  var JS_XSS_SANITIZERS = [
@@ -27950,13 +28254,13 @@ var TaintPropagationPass = class {
27950
28254
  }));
27951
28255
  const arrayFlows = detectArrayElementFlows(calls, sources, sinks, constProp.taintedArrayElements, constProp.unreachableLines, types) ?? [];
27952
28256
  for (const f of arrayFlows) {
27953
- 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)) {
27954
28258
  flows.push(f);
27955
28259
  }
27956
28260
  }
27957
28261
  const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
27958
28262
  for (const f of collectionFlows) {
27959
- 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;
27960
28264
  const flowForCheck = {
27961
28265
  source: { line: f.source_line },
27962
28266
  sink: { line: f.sink_line },
@@ -27975,7 +28279,7 @@ var TaintPropagationPass = class {
27975
28279
  }
27976
28280
  const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
27977
28281
  for (const f of paramFlows) {
27978
- 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)) {
27979
28283
  flows.push(f);
27980
28284
  }
27981
28285
  }
@@ -28365,7 +28669,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
28365
28669
  }
28366
28670
  const paramSource = methodParamSources.get(arg.variable);
28367
28671
  if (paramSource) {
28368
- 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);
28369
28673
  if (!exists) {
28370
28674
  if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
28371
28675
  continue;
@@ -11114,8 +11114,10 @@ var DEFAULT_SINKS = [
11114
11114
  { method: "newTransformer", class: "TransformerFactory", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
11115
11115
  { method: "transform", class: "Transformer", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
11116
11116
  // SSRF - Server-Side Request Forgery (CWE-918) and Open Redirect (CWE-601)
11117
- { method: "sendRedirect", class: "HttpServletResponse", type: "ssrf", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11118
- { method: "sendRedirect", type: "ssrf", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11117
+ // Sprint 82 (#189): HttpServletResponse.sendRedirect is CWE-601 / open_redirect,
11118
+ // not ssrf. Re-typing so manifest sink_type='open_redirect' matches.
11119
+ { method: "sendRedirect", class: "HttpServletResponse", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11120
+ { method: "sendRedirect", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11119
11121
  { method: "openConnection", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
11120
11122
  { method: "openStream", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
11121
11123
  // NOTE: URL/URI constructors removed — constructing a URL object doesn't make a network
@@ -11048,8 +11048,10 @@ var DEFAULT_SINKS = [
11048
11048
  { method: "newTransformer", class: "TransformerFactory", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
11049
11049
  { method: "transform", class: "Transformer", type: "xxe", cwe: "CWE-611", severity: "high", arg_positions: [0] },
11050
11050
  // SSRF - Server-Side Request Forgery (CWE-918) and Open Redirect (CWE-601)
11051
- { method: "sendRedirect", class: "HttpServletResponse", type: "ssrf", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11052
- { method: "sendRedirect", type: "ssrf", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11051
+ // Sprint 82 (#189): HttpServletResponse.sendRedirect is CWE-601 / open_redirect,
11052
+ // not ssrf. Re-typing so manifest sink_type='open_redirect' matches.
11053
+ { method: "sendRedirect", class: "HttpServletResponse", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11054
+ { method: "sendRedirect", type: "open_redirect", cwe: "CWE-601", severity: "high", arg_positions: [0] },
11053
11055
  { method: "openConnection", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
11054
11056
  { method: "openStream", class: "URL", type: "ssrf", cwe: "CWE-918", severity: "high", arg_positions: [] },
11055
11057
  // NOTE: URL/URI constructors removed — constructing a URL object doesn't make a network
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.131.0",
3
+ "version": "3.132.0",
4
4
  "description": "High-performance Static Application Security Testing (SAST) library for detecting security vulnerabilities through taint analysis",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",