circle-ir 3.131.0 → 3.133.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,18 @@ 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
+ }
23675
+ for (const finding of findGoPluginOpenCodeInjectionFindings(
23676
+ code,
23677
+ graph.ir.meta.file
23678
+ )) {
23679
+ ctx.addFinding(finding);
23680
+ }
23660
23681
  }
23661
23682
  if (language === "python") {
23662
23683
  additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
@@ -23675,6 +23696,18 @@ var LanguageSourcesPass = class {
23675
23696
  for (const finding of findPythonJinjaMarkupXssFindings(code, graph.ir.meta.file)) {
23676
23697
  ctx.addFinding(finding);
23677
23698
  }
23699
+ for (const finding of findPythonHeadersSubscriptOpenRedirectFindings(
23700
+ code,
23701
+ graph.ir.meta.file
23702
+ )) {
23703
+ ctx.addFinding(finding);
23704
+ }
23705
+ for (const finding of findPythonInteractiveInterpreterCodeInjectionFindings(
23706
+ code,
23707
+ graph.ir.meta.file
23708
+ )) {
23709
+ ctx.addFinding(finding);
23710
+ }
23678
23711
  }
23679
23712
  if (language === "rust") {
23680
23713
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
@@ -23696,6 +23729,18 @@ var LanguageSourcesPass = class {
23696
23729
  for (const finding of findRustWeakCryptoEcbFindings(code, graph.ir.meta.file)) {
23697
23730
  ctx.addFinding(finding);
23698
23731
  }
23732
+ for (const finding of findRustAppendHeaderTupleOpenRedirectFindings(
23733
+ code,
23734
+ graph.ir.meta.file
23735
+ )) {
23736
+ ctx.addFinding(finding);
23737
+ }
23738
+ for (const finding of findRustEvalCrateCodeInjectionFindings(
23739
+ code,
23740
+ graph.ir.meta.file
23741
+ )) {
23742
+ ctx.addFinding(finding);
23743
+ }
23699
23744
  }
23700
23745
  if (language === "javascript" || language === "typescript" || language === "htmljs") {
23701
23746
  additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
@@ -23714,6 +23759,18 @@ var LanguageSourcesPass = class {
23714
23759
  for (const finding of findTsAngularBypassXssFindings(code, graph.ir.meta.file)) {
23715
23760
  ctx.addFinding(finding);
23716
23761
  }
23762
+ for (const finding of findJsDomOpenRedirectFindings(
23763
+ code,
23764
+ graph.ir.meta.file
23765
+ )) {
23766
+ ctx.addFinding(finding);
23767
+ }
23768
+ for (const finding of findJsIndirectEvalCodeInjectionFindings(
23769
+ code,
23770
+ graph.ir.meta.file
23771
+ )) {
23772
+ ctx.addFinding(finding);
23773
+ }
23717
23774
  }
23718
23775
  if (language === "java") {
23719
23776
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
@@ -26616,6 +26673,691 @@ function findPythonJinjaMarkupXssFindings(code, file) {
26616
26673
  }
26617
26674
  return out2;
26618
26675
  }
26676
+ function findGoLocationHeaderOpenRedirectFindings(code, file) {
26677
+ const out2 = [];
26678
+ const lines = code.split("\n");
26679
+ const rwNames = /* @__PURE__ */ new Set();
26680
+ const sigRe = /\(\s*([A-Za-z_]\w*)\s+http\.ResponseWriter\b/g;
26681
+ for (const line of lines) {
26682
+ let m;
26683
+ sigRe.lastIndex = 0;
26684
+ while ((m = sigRe.exec(line)) !== null) rwNames.add(m[1]);
26685
+ }
26686
+ if (rwNames.size === 0) return out2;
26687
+ const safeWrapRe = /\b(?:net\/url|url)\.Parse\b[\s\S]{0,120}?\b(?:IsAbs|Host)\b|\bstrings\.HasPrefix\s*\(/;
26688
+ const callRe = /\b([A-Za-z_]\w*)\s*\.\s*Header\s*\(\s*\)\s*\.\s*(?:Set|Add)\s*\(\s*(['"])([^'"]+)\2\s*,\s*([\s\S]*)\)\s*(?:\/\/.*)?$/;
26689
+ for (let i2 = 0; i2 < lines.length; i2++) {
26690
+ const raw = lines[i2];
26691
+ const trimmed = raw.trim();
26692
+ if (!trimmed || trimmed.startsWith("//")) continue;
26693
+ const m = trimmed.match(callRe);
26694
+ if (!m) continue;
26695
+ const recv = m[1];
26696
+ if (!rwNames.has(recv)) continue;
26697
+ const key = m[3];
26698
+ if (key.toLowerCase() !== "location") continue;
26699
+ const valExpr = m[4];
26700
+ const valTrim = valExpr.trim();
26701
+ if (/^"(?:\\.|[^"\\])*"$/.test(valTrim)) continue;
26702
+ if (safeWrapRe.test(valExpr)) continue;
26703
+ out2.push({
26704
+ id: `open_redirect-${file}-${i2 + 1}`,
26705
+ pass: "language-sources",
26706
+ category: "security",
26707
+ rule_id: "open_redirect",
26708
+ cwe: "CWE-601",
26709
+ severity: "high",
26710
+ level: "error",
26711
+ 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.',
26712
+ file,
26713
+ line: i2 + 1,
26714
+ snippet: trimmed.substring(0, 100)
26715
+ });
26716
+ }
26717
+ return out2;
26718
+ }
26719
+ function findPythonHeadersSubscriptOpenRedirectFindings(code, file) {
26720
+ const out2 = [];
26721
+ const lines = code.split("\n");
26722
+ const requestSourceRe = /\b([A-Za-z_]\w*)\s*=\s*request\s*\.\s*(?:args|form|values|files|json|cookies|headers)(?:\s*\.\s*get\s*\(|\s*\[)/;
26723
+ const taintedVars = /* @__PURE__ */ new Set();
26724
+ for (const line of lines) {
26725
+ const m = line.match(requestSourceRe);
26726
+ if (m) taintedVars.add(m[1]);
26727
+ }
26728
+ const safeWrapRe = /\b(?:urllib\.parse\.urlparse|urlparse)\s*\([\s\S]{0,120}?\bnetloc\b|\b(?:startswith)\s*\(/;
26729
+ const subscriptRe = /\b([A-Za-z_]\w*)\s*\.\s*headers\s*\[\s*(['"])([^'"]+)\2\s*\]\s*=\s*(.+)$/;
26730
+ for (let i2 = 0; i2 < lines.length; i2++) {
26731
+ const raw = lines[i2];
26732
+ const trimmed = raw.trim();
26733
+ if (!trimmed || trimmed.startsWith("#")) continue;
26734
+ const m = trimmed.match(subscriptRe);
26735
+ if (!m) continue;
26736
+ const key = m[3];
26737
+ if (key.toLowerCase() !== "location") continue;
26738
+ const rhs = m[4].replace(/\s*(?:#.*)?$/, "").trim();
26739
+ if (/^['"](?:\\.|[^'"\\])*['"]$/.test(rhs)) continue;
26740
+ if (safeWrapRe.test(rhs)) continue;
26741
+ const hasTaintedVar = [...taintedVars].some(
26742
+ (v) => new RegExp(`\\b${v}\\b`).test(rhs)
26743
+ );
26744
+ const inlineSource = /\brequest\s*\.\s*(?:args|form|values|files|cookies|headers|json)(?:\s*\.\s*get\s*\(|\s*\[)/.test(
26745
+ rhs
26746
+ );
26747
+ if (!hasTaintedVar && !inlineSource) continue;
26748
+ out2.push({
26749
+ id: `open_redirect-${file}-${i2 + 1}`,
26750
+ pass: "language-sources",
26751
+ category: "security",
26752
+ rule_id: "open_redirect",
26753
+ cwe: "CWE-601",
26754
+ severity: "high",
26755
+ level: "error",
26756
+ 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.',
26757
+ file,
26758
+ line: i2 + 1,
26759
+ snippet: trimmed.substring(0, 100)
26760
+ });
26761
+ }
26762
+ return out2;
26763
+ }
26764
+ function findRustAppendHeaderTupleOpenRedirectFindings(code, file) {
26765
+ const out2 = [];
26766
+ const lines = code.split("\n");
26767
+ 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;
26768
+ const extractorParams = /* @__PURE__ */ new Set();
26769
+ for (const line of lines) {
26770
+ let m;
26771
+ extractorRe.lastIndex = 0;
26772
+ while ((m = extractorRe.exec(line)) !== null) {
26773
+ const name2 = m[1] ?? m[2];
26774
+ if (name2) extractorParams.add(name2);
26775
+ }
26776
+ }
26777
+ if (extractorParams.size === 0) return out2;
26778
+ const taintedVars = new Set(extractorParams);
26779
+ const letRe = /\blet\s+(?:mut\s+)?([A-Za-z_]\w*)\s*(?::[^=]+)?=\s*([^;]+);/g;
26780
+ for (let pass = 0; pass < 4; pass++) {
26781
+ const before = taintedVars.size;
26782
+ let lm;
26783
+ letRe.lastIndex = 0;
26784
+ while ((lm = letRe.exec(code)) !== null) {
26785
+ const name2 = lm[1];
26786
+ const rhs = lm[2];
26787
+ if (taintedVars.has(name2)) continue;
26788
+ for (const tv of taintedVars) {
26789
+ if (new RegExp(`\\b${tv}\\b`).test(rhs)) {
26790
+ taintedVars.add(name2);
26791
+ break;
26792
+ }
26793
+ }
26794
+ }
26795
+ if (taintedVars.size === before) break;
26796
+ }
26797
+ 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/;
26798
+ const tupleCallRe = /\.\s*(?:append_header|insert_header)\s*\(\s*\(\s*(['"])([^'"]+)\1\s*,\s*([^)]*?)\s*\)\s*\)/g;
26799
+ for (let i2 = 0; i2 < lines.length; i2++) {
26800
+ const raw = lines[i2];
26801
+ const trimmed = raw.trim();
26802
+ if (!trimmed || trimmed.startsWith("//")) continue;
26803
+ let cm;
26804
+ tupleCallRe.lastIndex = 0;
26805
+ while ((cm = tupleCallRe.exec(raw)) !== null) {
26806
+ const key = cm[2];
26807
+ if (key.toLowerCase() !== "location") continue;
26808
+ const valExpr = cm[3];
26809
+ if (/^['"](?:\\.|[^'"\\])*['"]$/.test(valExpr.trim())) continue;
26810
+ if (safeWrapRe.test(valExpr)) continue;
26811
+ const hasTainted = [...taintedVars].some(
26812
+ (v) => new RegExp(`\\b${v}\\b`).test(valExpr)
26813
+ );
26814
+ if (!hasTainted) continue;
26815
+ out2.push({
26816
+ id: `open_redirect-${file}-${i2 + 1}`,
26817
+ pass: "language-sources",
26818
+ category: "security",
26819
+ rule_id: "open_redirect",
26820
+ cwe: "CWE-601",
26821
+ severity: "high",
26822
+ level: "error",
26823
+ 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.',
26824
+ file,
26825
+ line: i2 + 1,
26826
+ snippet: trimmed.substring(0, 100)
26827
+ });
26828
+ break;
26829
+ }
26830
+ }
26831
+ return out2;
26832
+ }
26833
+ function findJsDomOpenRedirectFindings(code, file) {
26834
+ const out2 = [];
26835
+ const lines = code.split("\n");
26836
+ 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/;
26837
+ const taintedVars = /* @__PURE__ */ new Set();
26838
+ const assignRe = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([^;\n]+)/g;
26839
+ let am;
26840
+ assignRe.lastIndex = 0;
26841
+ while ((am = assignRe.exec(code)) !== null) {
26842
+ if (domSourceRe.test(am[2])) taintedVars.add(am[1]);
26843
+ }
26844
+ for (let pass = 0; pass < 4; pass++) {
26845
+ const before = taintedVars.size;
26846
+ assignRe.lastIndex = 0;
26847
+ while ((am = assignRe.exec(code)) !== null) {
26848
+ if (taintedVars.has(am[1])) continue;
26849
+ for (const tv of taintedVars) {
26850
+ if (new RegExp(`\\b${tv}\\b`).test(am[2])) {
26851
+ taintedVars.add(am[1]);
26852
+ break;
26853
+ }
26854
+ }
26855
+ }
26856
+ if (taintedVars.size === before) break;
26857
+ }
26858
+ const safeWrapRe = /\b(?:startsWith|includes)\s*\(\s*['"`]\/[^'"`]*['"`]\s*\)|\bnew\s+URL\s*\([\s\S]{0,80}?\)\s*\.\s*(?:origin|hostname)\s*===/;
26859
+ const containsTaint = (expr) => {
26860
+ if (domSourceRe.test(expr)) return true;
26861
+ for (const tv of taintedVars) {
26862
+ if (new RegExp(`\\b${tv}\\b`).test(expr)) return true;
26863
+ }
26864
+ return false;
26865
+ };
26866
+ const hrefSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*href\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
26867
+ const locSinkRe = /\b(?:window|document|self|top|parent)\s*\.\s*location\s*=\s*([^;\n]+?)\s*;?\s*(?:\/\/.*)?$/;
26868
+ const contentSinkRe = /\.\s*content\s*=\s*([^\n]+?)\s*$/;
26869
+ const callSinkRe = /\b(?:(?:window|document|self|top|parent)\s*\.\s*)?location\s*\.\s*(?:assign|replace)\s*\(\s*([^)]+)\)/;
26870
+ const emit = (line, msg, snippet) => {
26871
+ out2.push({
26872
+ id: `open_redirect-${file}-${line}`,
26873
+ pass: "language-sources",
26874
+ category: "security",
26875
+ rule_id: "open_redirect",
26876
+ cwe: "CWE-601",
26877
+ severity: "high",
26878
+ level: "error",
26879
+ message: msg,
26880
+ file,
26881
+ line,
26882
+ snippet: snippet.substring(0, 100)
26883
+ });
26884
+ };
26885
+ const isLiteralOnly = (expr) => /^['"`](?:\\.|[^'"`\\])*['"`]$/.test(expr.trim());
26886
+ for (let i2 = 0; i2 < lines.length; i2++) {
26887
+ const raw = lines[i2];
26888
+ const trimmed = raw.trim();
26889
+ if (!trimmed || trimmed.startsWith("//")) continue;
26890
+ let matched = false;
26891
+ const hm = trimmed.match(hrefSinkRe);
26892
+ if (hm) {
26893
+ const rhs = hm[1].trim();
26894
+ if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
26895
+ emit(
26896
+ i2 + 1,
26897
+ "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.",
26898
+ trimmed
26899
+ );
26900
+ matched = true;
26901
+ }
26902
+ }
26903
+ if (!matched) {
26904
+ const lm = trimmed.match(locSinkRe);
26905
+ if (lm) {
26906
+ const rhs = lm[1].trim();
26907
+ if (!isLiteralOnly(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
26908
+ emit(
26909
+ i2 + 1,
26910
+ "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.",
26911
+ trimmed
26912
+ );
26913
+ matched = true;
26914
+ }
26915
+ }
26916
+ }
26917
+ if (!matched) {
26918
+ const cm = trimmed.match(contentSinkRe);
26919
+ if (cm) {
26920
+ const rhs = cm[1].replace(/\s*\/\/.*$/, "").replace(/\s*;\s*$/, "").trim();
26921
+ if (!isLiteralOnly(rhs) && /['"`][^'"`]*\burl\s*=/i.test(rhs) && containsTaint(rhs) && !safeWrapRe.test(rhs)) {
26922
+ emit(
26923
+ i2 + 1,
26924
+ "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.",
26925
+ trimmed
26926
+ );
26927
+ matched = true;
26928
+ }
26929
+ }
26930
+ }
26931
+ if (!matched) {
26932
+ const callm = trimmed.match(callSinkRe);
26933
+ if (callm) {
26934
+ const arg = callm[1].trim();
26935
+ if (!isLiteralOnly(arg) && containsTaint(arg) && !safeWrapRe.test(arg)) {
26936
+ emit(
26937
+ i2 + 1,
26938
+ "Open redirect: location.assign / location.replace invoked with a value derived from a URL query/hash without allow-list validation. Validate origin before navigating.",
26939
+ trimmed
26940
+ );
26941
+ }
26942
+ }
26943
+ }
26944
+ }
26945
+ return out2;
26946
+ }
26947
+ function findGoPluginOpenCodeInjectionFindings(code, file) {
26948
+ const findings = [];
26949
+ if (typeof code !== "string" || code.length === 0) return findings;
26950
+ if (!/\bplugin\s*\.\s*(?:Open|Lookup)\s*\(/.test(code)) return findings;
26951
+ const lines = code.split("\n");
26952
+ const reqExtractRe = /\b\w+\s*\.\s*(?:FormValue|PostFormValue|URL\.Query\(\)\.Get|Header\.Get|Cookie)\s*\(/;
26953
+ const httpReqParamRe = /\*\s*http\.Request\b/;
26954
+ const callRe = /\bplugin\s*\.\s*(?:Open|Lookup)\s*\(\s*([^)]*)\s*\)/;
26955
+ const sinkLabel = (op) => op === "Lookup" ? "Go plugin.Lookup" : "Go plugin.Open";
26956
+ const funcs = [];
26957
+ let cur = null;
26958
+ for (let i2 = 0; i2 < lines.length; i2++) {
26959
+ const t = lines[i2].trim();
26960
+ if (/^func\b/.test(t)) {
26961
+ if (cur) {
26962
+ cur.end = i2 - 1;
26963
+ funcs.push(cur);
26964
+ }
26965
+ cur = { start: i2, end: lines.length - 1 };
26966
+ }
26967
+ }
26968
+ if (cur) funcs.push(cur);
26969
+ for (const fn of funcs) {
26970
+ const header = lines[fn.start];
26971
+ if (!httpReqParamRe.test(header)) continue;
26972
+ const taintedVars = /* @__PURE__ */ new Set();
26973
+ for (let pass = 0; pass < 3; pass++) {
26974
+ const before = taintedVars.size;
26975
+ for (let i2 = fn.start; i2 <= fn.end; i2++) {
26976
+ const line = lines[i2];
26977
+ const trimmed = line.trim();
26978
+ const assignMatch = trimmed.match(
26979
+ /^(\w+)\s*(?::=|=)\s*(.+?)(?:\s*\/\/.*)?$/
26980
+ );
26981
+ if (!assignMatch) continue;
26982
+ const lhs = assignMatch[1];
26983
+ const rhs = assignMatch[2];
26984
+ if (taintedVars.has(lhs)) continue;
26985
+ if (reqExtractRe.test(rhs)) {
26986
+ taintedVars.add(lhs);
26987
+ continue;
26988
+ }
26989
+ for (const v of taintedVars) {
26990
+ const re = new RegExp(`\\b${v}\\b`);
26991
+ if (re.test(rhs)) {
26992
+ taintedVars.add(lhs);
26993
+ break;
26994
+ }
26995
+ }
26996
+ }
26997
+ if (taintedVars.size === before) break;
26998
+ }
26999
+ if (taintedVars.size === 0) continue;
27000
+ for (let i2 = fn.start; i2 <= fn.end; i2++) {
27001
+ const line = lines[i2];
27002
+ const m = line.match(callRe);
27003
+ if (!m) continue;
27004
+ const arg = m[1].trim();
27005
+ if (arg.length === 0) continue;
27006
+ if (/^"[^"]*"$/.test(arg)) continue;
27007
+ let tainted = false;
27008
+ if (reqExtractRe.test(arg)) tainted = true;
27009
+ else {
27010
+ for (const v of taintedVars) {
27011
+ const re = new RegExp(`\\b${v}\\b`);
27012
+ if (re.test(arg)) {
27013
+ tainted = true;
27014
+ break;
27015
+ }
27016
+ }
27017
+ }
27018
+ if (!tainted) continue;
27019
+ const op = /\bplugin\s*\.\s*Lookup\b/.test(line) ? "Lookup" : "Open";
27020
+ findings.push({
27021
+ id: `code_injection-${file}-${i2 + 1}-go-plugin-${op.toLowerCase()}`,
27022
+ pass: "language-sources",
27023
+ category: "security",
27024
+ rule_id: "code_injection",
27025
+ cwe: "CWE-94",
27026
+ severity: "critical",
27027
+ level: "error",
27028
+ message: `Code injection: ${sinkLabel(op)} called with a path/symbol derived from an *http.Request without an allow-list. Loading a plugin runs its init() and exposes arbitrary exported symbols. Restrict the path to a trusted directory or use a fixed allow-list.`,
27029
+ file,
27030
+ line: i2 + 1,
27031
+ snippet: line.trim()
27032
+ });
27033
+ }
27034
+ }
27035
+ return findings;
27036
+ }
27037
+ function findJsIndirectEvalCodeInjectionFindings(code, file) {
27038
+ const findings = [];
27039
+ if (typeof code !== "string" || code.length === 0) return findings;
27040
+ if (!/\beval\b/.test(code)) return findings;
27041
+ const lines = code.split("\n");
27042
+ const reqExtractRe = /\breq(?:uest)?\s*\.\s*(?:body|query|params|headers|cookies)\b/;
27043
+ const aliasRe = /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(?:globalThis\s*\.\s*eval|global\s*\.\s*eval|window\s*\.\s*eval|self\s*\.\s*eval|eval)\s*;?\s*$/;
27044
+ const aliases = /* @__PURE__ */ new Set();
27045
+ for (const line of lines) {
27046
+ const m = line.match(aliasRe);
27047
+ if (m) aliases.add(m[1]);
27048
+ }
27049
+ const taintedVars = /* @__PURE__ */ new Set();
27050
+ const assignRe = /^\s*(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?\s*$/;
27051
+ const reassignRe = /^\s*(\w+)\s*=\s*(.+?);?\s*$/;
27052
+ for (let pass = 0; pass < 3; pass++) {
27053
+ const before = taintedVars.size;
27054
+ for (const line of lines) {
27055
+ const m = line.match(assignRe) || line.match(reassignRe);
27056
+ if (!m) continue;
27057
+ const lhs = m[1];
27058
+ const rhs = m[2];
27059
+ if (taintedVars.has(lhs)) continue;
27060
+ if (lhs === "const" || lhs === "let" || lhs === "var") continue;
27061
+ if (reqExtractRe.test(rhs)) {
27062
+ taintedVars.add(lhs);
27063
+ continue;
27064
+ }
27065
+ for (const v of taintedVars) {
27066
+ const re = new RegExp(`\\b${v}\\b`);
27067
+ if (re.test(rhs)) {
27068
+ taintedVars.add(lhs);
27069
+ break;
27070
+ }
27071
+ }
27072
+ }
27073
+ if (taintedVars.size === before) break;
27074
+ }
27075
+ const indirectCommaRe = /\(\s*0\s*,\s*eval\s*\)\s*\(\s*([^)]*)\s*\)/;
27076
+ const indirectMemberRe = /\b(?:globalThis|global|window|self)\s*\.\s*eval\s*\(\s*([^)]*)\s*\)/;
27077
+ for (let i2 = 0; i2 < lines.length; i2++) {
27078
+ const line = lines[i2];
27079
+ const trimmed = line.trim();
27080
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
27081
+ if (aliasRe.test(line)) continue;
27082
+ let arg = null;
27083
+ let formLabel = "";
27084
+ let m = trimmed.match(indirectCommaRe);
27085
+ if (m) {
27086
+ arg = m[1].trim();
27087
+ formLabel = "(0, eval)(...) indirect eval";
27088
+ }
27089
+ if (!arg) {
27090
+ m = trimmed.match(indirectMemberRe);
27091
+ if (m) {
27092
+ arg = m[1].trim();
27093
+ formLabel = "globalThis.eval / window.eval / self.eval indirect eval";
27094
+ }
27095
+ }
27096
+ if (!arg && aliases.size > 0) {
27097
+ for (const a of aliases) {
27098
+ const aliasCallRe = new RegExp(`\\b${a}\\s*\\(\\s*([^)]*)\\s*\\)`);
27099
+ const mm = trimmed.match(aliasCallRe);
27100
+ if (mm) {
27101
+ arg = mm[1].trim();
27102
+ formLabel = `aliased eval reference \`${a}(...)\``;
27103
+ break;
27104
+ }
27105
+ }
27106
+ }
27107
+ if (arg === null) continue;
27108
+ if (arg.length === 0) continue;
27109
+ if (/^['"`][^'"`]*['"`]$/.test(arg)) continue;
27110
+ let tainted = false;
27111
+ if (reqExtractRe.test(arg)) tainted = true;
27112
+ else {
27113
+ for (const v of taintedVars) {
27114
+ const re = new RegExp(`\\b${v}\\b`);
27115
+ if (re.test(arg)) {
27116
+ tainted = true;
27117
+ break;
27118
+ }
27119
+ }
27120
+ }
27121
+ if (!tainted) continue;
27122
+ findings.push({
27123
+ id: `code_injection-${file}-${i2 + 1}-js-indirect-eval`,
27124
+ pass: "language-sources",
27125
+ category: "security",
27126
+ rule_id: "code_injection",
27127
+ cwe: "CWE-94",
27128
+ severity: "critical",
27129
+ level: "error",
27130
+ message: `Code injection: ${formLabel} called with a value derived from an HTTP request body/query/headers. Indirect eval forms still execute arbitrary code in the global scope. Remove the eval and parse the input with a safe data parser instead.`,
27131
+ file,
27132
+ line: i2 + 1,
27133
+ snippet: trimmed
27134
+ });
27135
+ }
27136
+ return findings;
27137
+ }
27138
+ function findPythonInteractiveInterpreterCodeInjectionFindings(code, file) {
27139
+ const findings = [];
27140
+ if (typeof code !== "string" || code.length === 0) return findings;
27141
+ if (!/^\s*import\s+code\b/m.test(code)) return findings;
27142
+ if (!/\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole|compile_command)\b/.test(
27143
+ code
27144
+ )) {
27145
+ return findings;
27146
+ }
27147
+ const lines = code.split("\n");
27148
+ const reqExtractRe = /\brequest\s*\.\s*(?:args|form|values|files|json|cookies|headers|get_data|get_json)\b/;
27149
+ const taintedVars = /* @__PURE__ */ new Set();
27150
+ const assignRe = /^\s*(\w+)\s*=\s*(.+?)\s*(?:#.*)?$/;
27151
+ for (let pass = 0; pass < 3; pass++) {
27152
+ const before = taintedVars.size;
27153
+ for (const line of lines) {
27154
+ const m = line.match(assignRe);
27155
+ if (!m) continue;
27156
+ const lhs = m[1];
27157
+ const rhs = m[2];
27158
+ if (taintedVars.has(lhs)) continue;
27159
+ if (reqExtractRe.test(rhs)) {
27160
+ taintedVars.add(lhs);
27161
+ continue;
27162
+ }
27163
+ for (const v of taintedVars) {
27164
+ const re = new RegExp(`\\b${v}\\b`);
27165
+ if (re.test(rhs)) {
27166
+ taintedVars.add(lhs);
27167
+ break;
27168
+ }
27169
+ }
27170
+ }
27171
+ if (taintedVars.size === before) break;
27172
+ }
27173
+ const callRe = /\bcode\s*\.\s*(?:InteractiveInterpreter|InteractiveConsole)\s*\([^)]*\)\s*\.\s*(runsource|runcode|push|interact)\s*\(\s*([^),]+)/;
27174
+ const compileRe = /\bcode\s*\.\s*compile_command\s*\(\s*([^),]+)/;
27175
+ for (let i2 = 0; i2 < lines.length; i2++) {
27176
+ const line = lines[i2];
27177
+ const trimmed = line.trim();
27178
+ let arg = null;
27179
+ let formLabel = "";
27180
+ const m1 = trimmed.match(callRe);
27181
+ if (m1) {
27182
+ arg = m1[2].trim();
27183
+ formLabel = `code.${/Interpreter/.test(trimmed) ? "InteractiveInterpreter" : "InteractiveConsole"}().${m1[1]}`;
27184
+ }
27185
+ if (!arg) {
27186
+ const m2 = trimmed.match(compileRe);
27187
+ if (m2) {
27188
+ arg = m2[1].trim();
27189
+ formLabel = "code.compile_command";
27190
+ }
27191
+ }
27192
+ if (arg === null) continue;
27193
+ if (arg.length === 0) continue;
27194
+ if (/^['"][^'"]*['"]$/.test(arg)) continue;
27195
+ let tainted = false;
27196
+ if (reqExtractRe.test(arg)) tainted = true;
27197
+ else {
27198
+ for (const v of taintedVars) {
27199
+ const re = new RegExp(`\\b${v}\\b`);
27200
+ if (re.test(arg)) {
27201
+ tainted = true;
27202
+ break;
27203
+ }
27204
+ }
27205
+ }
27206
+ if (!tainted) continue;
27207
+ findings.push({
27208
+ id: `code_injection-${file}-${i2 + 1}-py-interactive-interpreter`,
27209
+ pass: "language-sources",
27210
+ category: "security",
27211
+ rule_id: "code_injection",
27212
+ cwe: "CWE-94",
27213
+ severity: "critical",
27214
+ level: "error",
27215
+ message: `Code injection: ${formLabel}(...) called with a value derived from a Flask request extractor. The Python \`code\` module compiles and executes arbitrary source. Remove the call and validate input against a fixed allow-list instead.`,
27216
+ file,
27217
+ line: i2 + 1,
27218
+ snippet: trimmed
27219
+ });
27220
+ }
27221
+ return findings;
27222
+ }
27223
+ function findRustEvalCrateCodeInjectionFindings(code, file) {
27224
+ const findings = [];
27225
+ if (typeof code !== "string" || code.length === 0) return findings;
27226
+ if (!/\b(?:evalexpr\s*::\s*eval|libloading\s*::\s*Library\s*::\s*new|\.\s*load\s*\([^)]*\)\s*\.\s*(?:exec|eval|call))/.test(
27227
+ code
27228
+ )) {
27229
+ return findings;
27230
+ }
27231
+ const lines = code.split("\n");
27232
+ const extractorTypeRe = /:\s*(?:String|Bytes|bytes::Bytes|axum::body::Bytes|web::Query\b|web::Path\b|web::Form\b|web::Json\b|HttpRequest\b|actix_web::HttpRequest\b)/;
27233
+ const fns = [];
27234
+ let cur = null;
27235
+ for (let i2 = 0; i2 < lines.length; i2++) {
27236
+ const t = lines[i2];
27237
+ if (/^\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+\s*\(/.test(t)) {
27238
+ if (cur) {
27239
+ cur.end = i2 - 1;
27240
+ fns.push(cur);
27241
+ }
27242
+ cur = { start: i2, end: lines.length - 1, tainted: /* @__PURE__ */ new Set() };
27243
+ const headerJoined = (() => {
27244
+ let j = i2;
27245
+ let s = "";
27246
+ while (j < lines.length && !/\{\s*$/.test(s)) {
27247
+ s += lines[j];
27248
+ if (/\{\s*$/.test(lines[j])) break;
27249
+ j++;
27250
+ if (j - i2 > 12) break;
27251
+ }
27252
+ return s;
27253
+ })();
27254
+ const open = headerJoined.indexOf("(");
27255
+ const close = headerJoined.lastIndexOf(")");
27256
+ if (open !== -1 && close > open) {
27257
+ const params = headerJoined.substring(open + 1, close);
27258
+ let depth = 0;
27259
+ let buf = "";
27260
+ const parts2 = [];
27261
+ for (const ch of params) {
27262
+ if (ch === "<" || ch === "(") depth++;
27263
+ else if (ch === ">" || ch === ")") depth--;
27264
+ if (ch === "," && depth === 0) {
27265
+ parts2.push(buf);
27266
+ buf = "";
27267
+ continue;
27268
+ }
27269
+ buf += ch;
27270
+ }
27271
+ if (buf.trim().length > 0) parts2.push(buf);
27272
+ for (const p of parts2) {
27273
+ const pm = p.match(/(?:mut\s+)?(\w+)\s*:/);
27274
+ if (!pm) continue;
27275
+ if (extractorTypeRe.test(p)) cur.tainted.add(pm[1]);
27276
+ }
27277
+ }
27278
+ }
27279
+ }
27280
+ if (cur) fns.push(cur);
27281
+ for (const fn of fns) {
27282
+ for (let pass = 0; pass < 3; pass++) {
27283
+ const before = fn.tainted.size;
27284
+ for (let i2 = fn.start; i2 <= fn.end; i2++) {
27285
+ const t = lines[i2].trim();
27286
+ const m = t.match(/^let\s+(?:mut\s+)?(\w+)\s*(?::\s*[^=]+)?=\s*(.+?);?$/);
27287
+ if (!m) continue;
27288
+ const lhs = m[1];
27289
+ const rhs = m[2];
27290
+ if (fn.tainted.has(lhs)) continue;
27291
+ for (const v of fn.tainted) {
27292
+ const re = new RegExp(`\\b${v}\\b`);
27293
+ if (re.test(rhs)) {
27294
+ fn.tainted.add(lhs);
27295
+ break;
27296
+ }
27297
+ }
27298
+ }
27299
+ if (fn.tainted.size === before) break;
27300
+ }
27301
+ }
27302
+ const evalExprRe = /\bevalexpr\s*::\s*eval(?:_with_context|_boolean|_int|_float|_string|_tuple|_empty)?\s*\(\s*([^)]+)\s*\)/;
27303
+ const libloadingRe = /\blibloading\s*::\s*Library\s*::\s*new\s*\(\s*([^)]+)\s*\)/;
27304
+ const luaLoadRe = /\.\s*load\s*\(\s*([^)]+)\s*\)\s*\.\s*(?:exec|eval|call)\b/;
27305
+ for (const fn of fns) {
27306
+ if (fn.tainted.size === 0) continue;
27307
+ for (let i2 = fn.start; i2 <= fn.end; i2++) {
27308
+ const line = lines[i2];
27309
+ const trimmed = line.trim();
27310
+ let arg = null;
27311
+ let formLabel = "";
27312
+ let m = trimmed.match(evalExprRe);
27313
+ if (m) {
27314
+ arg = m[1].trim();
27315
+ formLabel = "evalexpr::eval";
27316
+ }
27317
+ if (!arg) {
27318
+ m = trimmed.match(libloadingRe);
27319
+ if (m) {
27320
+ arg = m[1].trim();
27321
+ formLabel = "libloading::Library::new";
27322
+ }
27323
+ }
27324
+ if (!arg) {
27325
+ m = trimmed.match(luaLoadRe);
27326
+ if (m) {
27327
+ arg = m[1].trim();
27328
+ formLabel = "mlua/rlua Lua::load().{exec|eval|call}";
27329
+ }
27330
+ }
27331
+ if (arg === null) continue;
27332
+ if (arg.length === 0) continue;
27333
+ let unwrapped = arg.replace(/^&\s*/, "").trim();
27334
+ if (/^"[^"]*"$/.test(unwrapped)) continue;
27335
+ let tainted = false;
27336
+ for (const v of fn.tainted) {
27337
+ const re = new RegExp(`\\b${v}\\b`);
27338
+ if (re.test(unwrapped)) {
27339
+ tainted = true;
27340
+ break;
27341
+ }
27342
+ }
27343
+ if (!tainted) continue;
27344
+ findings.push({
27345
+ id: `code_injection-${file}-${i2 + 1}-rust-eval-crate`,
27346
+ pass: "language-sources",
27347
+ category: "security",
27348
+ rule_id: "code_injection",
27349
+ cwe: "CWE-94",
27350
+ severity: "critical",
27351
+ level: "error",
27352
+ message: `Code injection: ${formLabel}(...) called with a value derived from an HTTP request extractor (body / Query / Path / Form / Json / HttpRequest). The expression / library / Lua chunk is executed as code. Remove the dynamic-eval path or restrict input to a fixed allow-list.`,
27353
+ file,
27354
+ line: i2 + 1,
27355
+ snippet: trimmed
27356
+ });
27357
+ }
27358
+ }
27359
+ return findings;
27360
+ }
26619
27361
 
26620
27362
  // src/analysis/passes/sink-filter-pass.ts
26621
27363
  var JS_XSS_SANITIZERS = [
@@ -27950,13 +28692,13 @@ var TaintPropagationPass = class {
27950
28692
  }));
27951
28693
  const arrayFlows = detectArrayElementFlows(calls, sources, sinks, constProp.taintedArrayElements, constProp.unreachableLines, types) ?? [];
27952
28694
  for (const f of arrayFlows) {
27953
- if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
28695
+ if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
27954
28696
  flows.push(f);
27955
28697
  }
27956
28698
  }
27957
28699
  const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
27958
28700
  for (const f of collectionFlows) {
27959
- if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) continue;
28701
+ if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) continue;
27960
28702
  const flowForCheck = {
27961
28703
  source: { line: f.source_line },
27962
28704
  sink: { line: f.sink_line },
@@ -27975,7 +28717,7 @@ var TaintPropagationPass = class {
27975
28717
  }
27976
28718
  const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
27977
28719
  for (const f of paramFlows) {
27978
- if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
28720
+ if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
27979
28721
  flows.push(f);
27980
28722
  }
27981
28723
  }
@@ -28365,7 +29107,7 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
28365
29107
  }
28366
29108
  const paramSource = methodParamSources.get(arg.variable);
28367
29109
  if (paramSource) {
28368
- const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
29110
+ const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line && f.sink_type === sink.type);
28369
29111
  if (!exists) {
28370
29112
  if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
28371
29113
  continue;