circle-ir 3.135.0 → 3.137.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.
@@ -23684,6 +23684,12 @@ var LanguageSourcesPass = class {
23684
23684
  )) {
23685
23685
  ctx.addFinding(finding);
23686
23686
  }
23687
+ for (const finding of findGoLdapInjectionFindings(
23688
+ code,
23689
+ graph.ir.meta.file
23690
+ )) {
23691
+ ctx.addFinding(finding);
23692
+ }
23687
23693
  }
23688
23694
  if (language === "python") {
23689
23695
  additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
@@ -23720,6 +23726,18 @@ var LanguageSourcesPass = class {
23720
23726
  )) {
23721
23727
  ctx.addFinding(finding);
23722
23728
  }
23729
+ for (const finding of findPythonTaintedFormatStringFindings(
23730
+ code,
23731
+ graph.ir.meta.file
23732
+ )) {
23733
+ ctx.addFinding(finding);
23734
+ }
23735
+ for (const finding of findPythonHeaderCrlfInjectionFindings(
23736
+ code,
23737
+ graph.ir.meta.file
23738
+ )) {
23739
+ ctx.addFinding(finding);
23740
+ }
23723
23741
  }
23724
23742
  if (language === "rust") {
23725
23743
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
@@ -23753,6 +23771,18 @@ var LanguageSourcesPass = class {
23753
23771
  )) {
23754
23772
  ctx.addFinding(finding);
23755
23773
  }
23774
+ for (const finding of findRustLdapInjectionFindings(
23775
+ code,
23776
+ graph.ir.meta.file
23777
+ )) {
23778
+ ctx.addFinding(finding);
23779
+ }
23780
+ for (const finding of findRustLogInjectionFindings(
23781
+ code,
23782
+ graph.ir.meta.file
23783
+ )) {
23784
+ ctx.addFinding(finding);
23785
+ }
23756
23786
  }
23757
23787
  if (language === "javascript" || language === "typescript" || language === "htmljs") {
23758
23788
  additionalSanitizers.push(...findJsSafeJsonParseSanitizers(code));
@@ -23783,6 +23813,18 @@ var LanguageSourcesPass = class {
23783
23813
  )) {
23784
23814
  ctx.addFinding(finding);
23785
23815
  }
23816
+ for (const finding of findJsUtilFormatFormatStringFindings(
23817
+ code,
23818
+ graph.ir.meta.file
23819
+ )) {
23820
+ ctx.addFinding(finding);
23821
+ }
23822
+ for (const finding of findJsLdapInjectionFindings(
23823
+ code,
23824
+ graph.ir.meta.file
23825
+ )) {
23826
+ ctx.addFinding(finding);
23827
+ }
23786
23828
  }
23787
23829
  if (language === "java") {
23788
23830
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
@@ -27686,6 +27728,732 @@ function findJavaUrlOpenStreamSsrfFindings(code, file) {
27686
27728
  }
27687
27729
  return findings;
27688
27730
  }
27731
+ function findPythonTaintedFormatStringFindings(code, file) {
27732
+ const findings = [];
27733
+ if (typeof code !== "string" || code.length === 0) return findings;
27734
+ if (!/\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)|flask\.request)\b/.test(
27735
+ code
27736
+ )) {
27737
+ return findings;
27738
+ }
27739
+ const lines = code.split("\n");
27740
+ const reqExtractRe = /\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)\b|flask\.request\b)/;
27741
+ const taintedVars = /* @__PURE__ */ new Set();
27742
+ for (let pass = 0; pass < 3; pass++) {
27743
+ const before = taintedVars.size;
27744
+ for (let i2 = 0; i2 < lines.length; i2++) {
27745
+ const t = lines[i2].trim();
27746
+ if (t.startsWith("#")) continue;
27747
+ const a = t.match(/^(\w+)\s*=\s*(.+?)$/);
27748
+ if (!a) continue;
27749
+ const lhs = a[1];
27750
+ const rhs = a[2];
27751
+ if (taintedVars.has(lhs)) continue;
27752
+ if (reqExtractRe.test(rhs)) {
27753
+ taintedVars.add(lhs);
27754
+ continue;
27755
+ }
27756
+ for (const v of taintedVars) {
27757
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27758
+ taintedVars.add(lhs);
27759
+ break;
27760
+ }
27761
+ }
27762
+ }
27763
+ if (taintedVars.size === before) break;
27764
+ }
27765
+ if (taintedVars.size === 0) return findings;
27766
+ const percentRe = /\b(\w+)\s*%\s*[\(\[\{"'\w]/;
27767
+ const dotFormatRe = /\b(\w+)\s*\.\s*format\s*\(/;
27768
+ const seen = /* @__PURE__ */ new Set();
27769
+ for (let i2 = 0; i2 < lines.length; i2++) {
27770
+ const line = lines[i2];
27771
+ const t = line.trim();
27772
+ if (t.startsWith("#")) continue;
27773
+ const pm = line.match(percentRe);
27774
+ if (pm && taintedVars.has(pm[1])) {
27775
+ const key = `${i2 + 1}:percent`;
27776
+ if (!seen.has(key)) {
27777
+ seen.add(key);
27778
+ findings.push({
27779
+ id: `format_string-${file}-${i2 + 1}-py-percent`,
27780
+ pass: "language-sources",
27781
+ category: "security",
27782
+ rule_id: "format_string",
27783
+ cwe: "CWE-134",
27784
+ severity: "high",
27785
+ level: "error",
27786
+ message: "Format-string injection: Python `<tainted> % args` uses an HTTP-request-controlled format string. Malformed specifiers can crash the handler (TypeError) and `%(name)s` access can reveal arbitrary mapping keys. Use a literal format string and pass untrusted values as arguments only.",
27787
+ file,
27788
+ line: i2 + 1,
27789
+ snippet: line.trim()
27790
+ });
27791
+ }
27792
+ }
27793
+ const dm = line.match(dotFormatRe);
27794
+ if (dm && taintedVars.has(dm[1])) {
27795
+ const key = `${i2 + 1}:dotformat`;
27796
+ if (!seen.has(key)) {
27797
+ seen.add(key);
27798
+ findings.push({
27799
+ id: `format_string-${file}-${i2 + 1}-py-strformat`,
27800
+ pass: "language-sources",
27801
+ category: "security",
27802
+ rule_id: "format_string",
27803
+ cwe: "CWE-134",
27804
+ severity: "high",
27805
+ level: "error",
27806
+ message: "Format-string injection: Python `<tainted>.format(...)` lets the attacker control the format template. Field-access shapes such as `{0.__class__.__init__.__globals__}` can leak module globals (e.g. secret keys). Use a literal template and pass untrusted values as positional/keyword arguments only.",
27807
+ file,
27808
+ line: i2 + 1,
27809
+ snippet: line.trim()
27810
+ });
27811
+ }
27812
+ }
27813
+ }
27814
+ return findings;
27815
+ }
27816
+ function findJsUtilFormatFormatStringFindings(code, file) {
27817
+ const findings = [];
27818
+ if (typeof code !== "string" || code.length === 0) return findings;
27819
+ if (!/\butil\s*\.\s*format\s*\(/.test(code)) return findings;
27820
+ if (!/\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/.test(code)) {
27821
+ return findings;
27822
+ }
27823
+ const lines = code.split("\n");
27824
+ const reqExtractRe = /\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/;
27825
+ const taintedVars = /* @__PURE__ */ new Set();
27826
+ for (let pass = 0; pass < 3; pass++) {
27827
+ const before = taintedVars.size;
27828
+ for (let i2 = 0; i2 < lines.length; i2++) {
27829
+ const t = lines[i2].trim();
27830
+ if (t.startsWith("//")) continue;
27831
+ const a = t.match(/^(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?$/);
27832
+ if (!a) continue;
27833
+ const lhs = a[1];
27834
+ const rhs = a[2];
27835
+ if (taintedVars.has(lhs)) continue;
27836
+ if (reqExtractRe.test(rhs)) {
27837
+ taintedVars.add(lhs);
27838
+ continue;
27839
+ }
27840
+ for (const v of taintedVars) {
27841
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27842
+ taintedVars.add(lhs);
27843
+ break;
27844
+ }
27845
+ }
27846
+ }
27847
+ if (taintedVars.size === before) break;
27848
+ }
27849
+ if (taintedVars.size === 0) return findings;
27850
+ const callRe = /\butil\s*\.\s*format\s*\(\s*([\w.]+)\s*[,)]/;
27851
+ for (let i2 = 0; i2 < lines.length; i2++) {
27852
+ const line = lines[i2];
27853
+ const m = line.match(callRe);
27854
+ if (!m) continue;
27855
+ const arg = m[1];
27856
+ const root = arg.split(".")[0];
27857
+ if (!taintedVars.has(root) && !reqExtractRe.test(arg)) continue;
27858
+ findings.push({
27859
+ id: `format_string-${file}-${i2 + 1}-js-util-format`,
27860
+ pass: "language-sources",
27861
+ category: "security",
27862
+ rule_id: "format_string",
27863
+ cwe: "CWE-134",
27864
+ severity: "medium",
27865
+ level: "error",
27866
+ message: "Format-string injection: Node `util.format(<tainted>, ...)` uses an HTTP-request-controlled format string. The attacker can manipulate `%s`/`%j`/`%O` specifiers to alter the rendered output. Pass user input as a subsequent argument with a literal format string.",
27867
+ file,
27868
+ line: i2 + 1,
27869
+ snippet: line.trim()
27870
+ });
27871
+ }
27872
+ return findings;
27873
+ }
27874
+ function findPythonHeaderCrlfInjectionFindings(code, file) {
27875
+ const findings = [];
27876
+ if (typeof code !== "string" || code.length === 0) return findings;
27877
+ if (!/\b\w+\s*\.\s*headers\b/.test(code)) return findings;
27878
+ if (!/\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)|flask\.request)\b/.test(
27879
+ code
27880
+ )) {
27881
+ return findings;
27882
+ }
27883
+ const lines = code.split("\n");
27884
+ const reqExtractRe = /\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)\b|flask\.request\b)/;
27885
+ const taintedVars = /* @__PURE__ */ new Set();
27886
+ for (let pass = 0; pass < 3; pass++) {
27887
+ const before = taintedVars.size;
27888
+ for (let i2 = 0; i2 < lines.length; i2++) {
27889
+ const t = lines[i2].trim();
27890
+ if (t.startsWith("#")) continue;
27891
+ const a = t.match(/^(\w+)\s*=\s*(.+?)$/);
27892
+ if (!a) continue;
27893
+ const lhs = a[1];
27894
+ const rhs = a[2];
27895
+ if (taintedVars.has(lhs)) continue;
27896
+ if (reqExtractRe.test(rhs)) {
27897
+ taintedVars.add(lhs);
27898
+ continue;
27899
+ }
27900
+ for (const v of taintedVars) {
27901
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27902
+ taintedVars.add(lhs);
27903
+ break;
27904
+ }
27905
+ }
27906
+ }
27907
+ if (taintedVars.size === before) break;
27908
+ }
27909
+ if (taintedVars.size === 0) return findings;
27910
+ const subscriptRe = /\b\w+\s*\.\s*headers\s*\[\s*['"][^'"]+['"]\s*\]\s*=\s*(.+)$/;
27911
+ const methodRe = /\b\w+\s*\.\s*headers\s*\.\s*(?:add|set|setdefault|append)\s*\(\s*['"][^'"]+['"]\s*,\s*(.+?)\)/;
27912
+ for (let i2 = 0; i2 < lines.length; i2++) {
27913
+ const line = lines[i2];
27914
+ const t = line.trim();
27915
+ if (t.startsWith("#")) continue;
27916
+ let expr = null;
27917
+ const sm = line.match(subscriptRe);
27918
+ if (sm) expr = sm[1];
27919
+ else {
27920
+ const mm = line.match(methodRe);
27921
+ if (mm) expr = mm[1];
27922
+ }
27923
+ if (!expr) continue;
27924
+ let tainted = reqExtractRe.test(expr);
27925
+ if (!tainted) {
27926
+ for (const v of taintedVars) {
27927
+ if (new RegExp(`\\b${v}\\b`).test(expr)) {
27928
+ tainted = true;
27929
+ break;
27930
+ }
27931
+ }
27932
+ }
27933
+ if (!tainted) continue;
27934
+ findings.push({
27935
+ id: `crlf-${file}-${i2 + 1}-py-headers`,
27936
+ pass: "language-sources",
27937
+ category: "security",
27938
+ rule_id: "crlf",
27939
+ cwe: "CWE-113",
27940
+ severity: "medium",
27941
+ level: "error",
27942
+ message: "CRLF / header injection: Python Flask/Werkzeug `response.headers[...] = <tainted>` lets an attacker inject a `\\r\\n` sequence and forge additional headers or split the response body. Validate the value (reject control characters) or use a fixed allowlist.",
27943
+ file,
27944
+ line: i2 + 1,
27945
+ snippet: line.trim()
27946
+ });
27947
+ }
27948
+ return findings;
27949
+ }
27950
+ function findJsLdapInjectionFindings(code, file) {
27951
+ const findings = [];
27952
+ if (typeof code !== "string" || code.length === 0) return findings;
27953
+ if (!/\.\s*search\s*\(/.test(code)) return findings;
27954
+ if (!/\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/.test(code)) {
27955
+ return findings;
27956
+ }
27957
+ if (!/\b(?:ldapjs|ldapts|require\(['"]ldapjs['"]\)|from\s+['"]ldapts['"])\b/.test(
27958
+ code
27959
+ )) {
27960
+ return findings;
27961
+ }
27962
+ const lines = code.split("\n");
27963
+ const reqExtractRe = /\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/;
27964
+ const taintedVars = /* @__PURE__ */ new Set();
27965
+ for (let pass = 0; pass < 3; pass++) {
27966
+ const before = taintedVars.size;
27967
+ for (let i2 = 0; i2 < lines.length; i2++) {
27968
+ const t = lines[i2].trim();
27969
+ if (t.startsWith("//")) continue;
27970
+ const m = t.match(/^(?:const|let|var)\s+(\w+)(?:\s*:\s*[^=]+)?\s*=\s*(.+?);?$/);
27971
+ if (!m) continue;
27972
+ const lhs = m[1];
27973
+ const rhs = m[2];
27974
+ if (taintedVars.has(lhs)) continue;
27975
+ if (reqExtractRe.test(rhs)) {
27976
+ taintedVars.add(lhs);
27977
+ continue;
27978
+ }
27979
+ for (const v of taintedVars) {
27980
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27981
+ taintedVars.add(lhs);
27982
+ break;
27983
+ }
27984
+ }
27985
+ }
27986
+ if (taintedVars.size === before) break;
27987
+ }
27988
+ if (taintedVars.size === 0) return findings;
27989
+ const filterPropRe = /\bfilter\s*:\s*([^,}\n]+)/;
27990
+ const filterShorthandRe = /(?:^|[\{,])\s*filter\s*[,}]/;
27991
+ const seen = /* @__PURE__ */ new Set();
27992
+ for (let i2 = 0; i2 < lines.length; i2++) {
27993
+ const line = lines[i2];
27994
+ const fm = line.match(filterPropRe);
27995
+ let expr = null;
27996
+ if (fm) {
27997
+ expr = fm[1].trim();
27998
+ } else if (filterShorthandRe.test(line)) {
27999
+ expr = "filter";
28000
+ } else {
28001
+ continue;
28002
+ }
28003
+ let tainted = false;
28004
+ for (const v of taintedVars) {
28005
+ if (new RegExp(`\\b${v}\\b`).test(expr)) {
28006
+ tainted = true;
28007
+ break;
28008
+ }
28009
+ }
28010
+ if (!tainted) continue;
28011
+ if (seen.has(i2 + 1)) continue;
28012
+ seen.add(i2 + 1);
28013
+ findings.push({
28014
+ id: `ldap_injection-${file}-${i2 + 1}-js-ldap-filter`,
28015
+ pass: "language-sources",
28016
+ category: "security",
28017
+ rule_id: "ldap_injection",
28018
+ cwe: "CWE-90",
28019
+ severity: "critical",
28020
+ level: "error",
28021
+ message: "LDAP injection: ldapjs/ldapts `client.search(..., { filter: <tainted>, ... })` lets the attacker break out of the filter expression (e.g. `*)(uid=*`) and enumerate the directory. Escape the user input with a tight allowlist (`[A-Za-z0-9_-]+`) or use a structured filter builder.",
28022
+ file,
28023
+ line: i2 + 1,
28024
+ snippet: line.trim()
28025
+ });
28026
+ }
28027
+ return findings;
28028
+ }
28029
+ function findGoLdapInjectionFindings(code, file) {
28030
+ const findings = [];
28031
+ if (typeof code !== "string" || code.length === 0) return findings;
28032
+ if (!/\bldap\s*\.\s*NewSearchRequest\s*\(/.test(code)) return findings;
28033
+ if (!/\br\s*\.\s*(?:URL\s*\.\s*Query\s*\(\s*\)|Form|PostForm|FormValue|PostFormValue|Header)/.test(
28034
+ code
28035
+ )) {
28036
+ return findings;
28037
+ }
28038
+ const lines = code.split("\n");
28039
+ const reqExtractRe = /\br\s*\.\s*(?:URL\s*\.\s*Query\s*\(\s*\)\s*\.\s*Get|Form\s*\.\s*Get|PostForm\s*\.\s*Get|FormValue|PostFormValue|Header\s*\.\s*Get)\s*\(/;
28040
+ const taintedVars = /* @__PURE__ */ new Set();
28041
+ for (let pass = 0; pass < 3; pass++) {
28042
+ const before = taintedVars.size;
28043
+ for (let i2 = 0; i2 < lines.length; i2++) {
28044
+ const t = lines[i2].trim();
28045
+ if (t.startsWith("//")) continue;
28046
+ const m = t.match(/^(\w+)\s*(?::=|=)\s*(.+?)$/);
28047
+ if (!m) continue;
28048
+ const lhs = m[1];
28049
+ const rhs = m[2];
28050
+ if (taintedVars.has(lhs)) continue;
28051
+ if (reqExtractRe.test(rhs)) {
28052
+ taintedVars.add(lhs);
28053
+ continue;
28054
+ }
28055
+ for (const v of taintedVars) {
28056
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
28057
+ taintedVars.add(lhs);
28058
+ break;
28059
+ }
28060
+ }
28061
+ }
28062
+ if (taintedVars.size === before) break;
28063
+ }
28064
+ if (taintedVars.size === 0) return findings;
28065
+ const seen = /* @__PURE__ */ new Set();
28066
+ for (let i2 = 0; i2 < lines.length; i2++) {
28067
+ const openIdx = lines[i2].indexOf("NewSearchRequest(");
28068
+ if (openIdx === -1) continue;
28069
+ let depth = 0;
28070
+ let buf = "";
28071
+ let endLine = i2;
28072
+ let started = false;
28073
+ for (let j = i2; j < Math.min(i2 + 25, lines.length); j++) {
28074
+ for (const ch of lines[j]) {
28075
+ if (ch === "(") {
28076
+ depth++;
28077
+ started = true;
28078
+ } else if (ch === ")") {
28079
+ depth--;
28080
+ }
28081
+ buf += ch;
28082
+ if (started && depth === 0) {
28083
+ endLine = j;
28084
+ break;
28085
+ }
28086
+ }
28087
+ if (started && depth === 0) break;
28088
+ buf += "\n";
28089
+ }
28090
+ const argsStart = buf.indexOf("(");
28091
+ if (argsStart === -1) continue;
28092
+ const args2 = buf.substring(argsStart + 1, buf.length - 1);
28093
+ const parts2 = [];
28094
+ {
28095
+ let d = 0;
28096
+ let b = "";
28097
+ let inStr = null;
28098
+ for (let k = 0; k < args2.length; k++) {
28099
+ const ch = args2[k];
28100
+ if (inStr) {
28101
+ if (ch === "\\") {
28102
+ b += ch + (args2[k + 1] ?? "");
28103
+ k++;
28104
+ continue;
28105
+ }
28106
+ if (ch === inStr) inStr = null;
28107
+ b += ch;
28108
+ continue;
28109
+ }
28110
+ if (ch === '"' || ch === "`") {
28111
+ inStr = ch;
28112
+ b += ch;
28113
+ continue;
28114
+ }
28115
+ if (ch === "(" || ch === "[" || ch === "{") d++;
28116
+ else if (ch === ")" || ch === "]" || ch === "}") d--;
28117
+ if (ch === "," && d === 0) {
28118
+ parts2.push(b);
28119
+ b = "";
28120
+ continue;
28121
+ }
28122
+ b += ch;
28123
+ }
28124
+ if (b.trim().length > 0) parts2.push(b);
28125
+ }
28126
+ if (parts2.length < 7) continue;
28127
+ const filterExpr = parts2[6].trim();
28128
+ let tainted = false;
28129
+ for (const v of taintedVars) {
28130
+ if (new RegExp(`\\b${v}\\b`).test(filterExpr)) {
28131
+ tainted = true;
28132
+ break;
28133
+ }
28134
+ }
28135
+ if (!tainted) continue;
28136
+ if (seen.has(i2 + 1)) continue;
28137
+ seen.add(i2 + 1);
28138
+ findings.push({
28139
+ id: `ldap_injection-${file}-${i2 + 1}-go-newsearchrequest`,
28140
+ pass: "language-sources",
28141
+ category: "security",
28142
+ rule_id: "ldap_injection",
28143
+ cwe: "CWE-90",
28144
+ severity: "critical",
28145
+ level: "error",
28146
+ message: "LDAP injection: go-ldap `ldap.NewSearchRequest(..., <tainted filter>, ...)` lets the attacker break out of the filter expression and enumerate the directory. Escape the user input with `ldap.EscapeFilter(...)` or use a structured filter builder.",
28147
+ file,
28148
+ line: i2 + 1,
28149
+ snippet: (lines[i2] + (endLine > i2 ? " \u2026" : "")).trim()
28150
+ });
28151
+ }
28152
+ return findings;
28153
+ }
28154
+ function findRustLdapInjectionFindings(code, file) {
28155
+ const findings = [];
28156
+ if (typeof code !== "string" || code.length === 0) return findings;
28157
+ if (!/\.\s*search\s*\(/.test(code)) return findings;
28158
+ if (!/\b(?:ldap3|LdapConn|Ldap)\b/.test(code)) return findings;
28159
+ const lines = code.split("\n");
28160
+ 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)/;
28161
+ const fns = [];
28162
+ let cur = null;
28163
+ for (let i2 = 0; i2 < lines.length; i2++) {
28164
+ const t = lines[i2];
28165
+ if (/^\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+\s*\(/.test(t)) {
28166
+ if (cur) {
28167
+ cur.end = i2 - 1;
28168
+ fns.push(cur);
28169
+ }
28170
+ cur = { start: i2, end: lines.length - 1, tainted: /* @__PURE__ */ new Set() };
28171
+ let headerJoined = "";
28172
+ for (let j = i2; j < Math.min(i2 + 12, lines.length); j++) {
28173
+ headerJoined += lines[j];
28174
+ if (/\{\s*$/.test(lines[j])) break;
28175
+ }
28176
+ const open = headerJoined.indexOf("(");
28177
+ const close = headerJoined.lastIndexOf(")");
28178
+ if (open !== -1 && close > open) {
28179
+ const params = headerJoined.substring(open + 1, close);
28180
+ let depth = 0;
28181
+ let buf = "";
28182
+ const parts2 = [];
28183
+ for (const ch of params) {
28184
+ if (ch === "<" || ch === "(") depth++;
28185
+ else if (ch === ">" || ch === ")") depth--;
28186
+ if (ch === "," && depth === 0) {
28187
+ parts2.push(buf);
28188
+ buf = "";
28189
+ continue;
28190
+ }
28191
+ buf += ch;
28192
+ }
28193
+ if (buf.trim().length > 0) parts2.push(buf);
28194
+ for (const p of parts2) {
28195
+ const pm = p.match(/(?:mut\s+)?(\w+)\s*:/);
28196
+ if (!pm) continue;
28197
+ if (extractorTypeRe.test(p)) cur.tainted.add(pm[1]);
28198
+ }
28199
+ }
28200
+ }
28201
+ }
28202
+ if (cur) fns.push(cur);
28203
+ for (const fn of fns) {
28204
+ for (let pass = 0; pass < 3; pass++) {
28205
+ const before = fn.tainted.size;
28206
+ for (let i2 = fn.start; i2 <= fn.end; i2++) {
28207
+ const t = lines[i2].trim();
28208
+ const m = t.match(/^let\s+(?:mut\s+)?(\w+)\s*(?::\s*[^=]+)?=\s*(.+?);?$/);
28209
+ if (!m) continue;
28210
+ const lhs = m[1];
28211
+ const rhs = m[2];
28212
+ if (fn.tainted.has(lhs)) continue;
28213
+ for (const v of fn.tainted) {
28214
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
28215
+ fn.tainted.add(lhs);
28216
+ break;
28217
+ }
28218
+ }
28219
+ }
28220
+ if (fn.tainted.size === before) break;
28221
+ }
28222
+ }
28223
+ const seen = /* @__PURE__ */ new Set();
28224
+ for (const fn of fns) {
28225
+ if (fn.tainted.size === 0) continue;
28226
+ for (let i2 = fn.start; i2 <= fn.end; i2++) {
28227
+ const searchIdx = lines[i2].indexOf(".search(");
28228
+ if (searchIdx === -1) continue;
28229
+ let depth = 0;
28230
+ let buf = "";
28231
+ let started = false;
28232
+ for (let j = i2; j < Math.min(i2 + 15, lines.length); j++) {
28233
+ const startCol = j === i2 ? searchIdx : 0;
28234
+ for (let k = startCol; k < lines[j].length; k++) {
28235
+ const ch = lines[j][k];
28236
+ if (ch === "(") {
28237
+ depth++;
28238
+ started = true;
28239
+ } else if (ch === ")") {
28240
+ depth--;
28241
+ }
28242
+ buf += ch;
28243
+ if (started && depth === 0) break;
28244
+ }
28245
+ if (started && depth === 0) break;
28246
+ buf += "\n";
28247
+ }
28248
+ const argsStart = buf.indexOf("(");
28249
+ if (argsStart === -1) continue;
28250
+ const args2 = buf.substring(argsStart + 1, buf.length - 1);
28251
+ const parts2 = [];
28252
+ {
28253
+ let d = 0;
28254
+ let b = "";
28255
+ let inStr = null;
28256
+ for (let k = 0; k < args2.length; k++) {
28257
+ const ch = args2[k];
28258
+ if (inStr) {
28259
+ if (ch === "\\") {
28260
+ b += ch + (args2[k + 1] ?? "");
28261
+ k++;
28262
+ continue;
28263
+ }
28264
+ if (ch === inStr) inStr = null;
28265
+ b += ch;
28266
+ continue;
28267
+ }
28268
+ if (ch === '"') {
28269
+ inStr = ch;
28270
+ b += ch;
28271
+ continue;
28272
+ }
28273
+ if (ch === "(" || ch === "[" || ch === "{") d++;
28274
+ else if (ch === ")" || ch === "]" || ch === "}") d--;
28275
+ if (ch === "," && d === 0) {
28276
+ parts2.push(b);
28277
+ b = "";
28278
+ continue;
28279
+ }
28280
+ b += ch;
28281
+ }
28282
+ if (b.trim().length > 0) parts2.push(b);
28283
+ }
28284
+ if (parts2.length < 3) continue;
28285
+ const filterExpr = parts2[2].trim();
28286
+ let tainted = false;
28287
+ for (const v of fn.tainted) {
28288
+ if (new RegExp(`\\b${v}\\b`).test(filterExpr)) {
28289
+ tainted = true;
28290
+ break;
28291
+ }
28292
+ }
28293
+ if (!tainted) continue;
28294
+ if (seen.has(i2 + 1)) continue;
28295
+ seen.add(i2 + 1);
28296
+ findings.push({
28297
+ id: `ldap_injection-${file}-${i2 + 1}-rust-ldap3-search`,
28298
+ pass: "language-sources",
28299
+ category: "security",
28300
+ rule_id: "ldap_injection",
28301
+ cwe: "CWE-90",
28302
+ severity: "critical",
28303
+ level: "error",
28304
+ message: "LDAP injection: ldap3 `LdapConn::search(..., &<tainted filter>, ...)` lets the attacker break out of the filter expression and enumerate the directory. Escape the user input or use a structured filter builder.",
28305
+ file,
28306
+ line: i2 + 1,
28307
+ snippet: lines[i2].trim()
28308
+ });
28309
+ }
28310
+ }
28311
+ return findings;
28312
+ }
28313
+ function findRustLogInjectionFindings(code, file) {
28314
+ const findings = [];
28315
+ if (typeof code !== "string" || code.length === 0) return findings;
28316
+ if (!/\b(?:info|warn|error|debug|trace)\s*!\s*\(/.test(code)) return findings;
28317
+ if (!/\b(?:log|tracing)\b/.test(code)) return findings;
28318
+ const lines = code.split("\n");
28319
+ 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)/;
28320
+ const fns = [];
28321
+ let cur = null;
28322
+ for (let i2 = 0; i2 < lines.length; i2++) {
28323
+ const t = lines[i2];
28324
+ if (/^\s*(?:pub\s+)?(?:async\s+)?fn\s+\w+\s*\(/.test(t)) {
28325
+ if (cur) {
28326
+ cur.end = i2 - 1;
28327
+ fns.push(cur);
28328
+ }
28329
+ cur = { start: i2, end: lines.length - 1, tainted: /* @__PURE__ */ new Set() };
28330
+ let headerJoined = "";
28331
+ for (let j = i2; j < Math.min(i2 + 12, lines.length); j++) {
28332
+ headerJoined += lines[j];
28333
+ if (/\{\s*$/.test(lines[j])) break;
28334
+ }
28335
+ const open = headerJoined.indexOf("(");
28336
+ const close = headerJoined.lastIndexOf(")");
28337
+ if (open !== -1 && close > open) {
28338
+ const params = headerJoined.substring(open + 1, close);
28339
+ let depth = 0;
28340
+ let buf = "";
28341
+ const parts2 = [];
28342
+ for (const ch of params) {
28343
+ if (ch === "<" || ch === "(") depth++;
28344
+ else if (ch === ">" || ch === ")") depth--;
28345
+ if (ch === "," && depth === 0) {
28346
+ parts2.push(buf);
28347
+ buf = "";
28348
+ continue;
28349
+ }
28350
+ buf += ch;
28351
+ }
28352
+ if (buf.trim().length > 0) parts2.push(buf);
28353
+ for (const p of parts2) {
28354
+ const pm = p.match(/(?:mut\s+)?(\w+)\s*:/);
28355
+ if (!pm) continue;
28356
+ if (extractorTypeRe.test(p)) cur.tainted.add(pm[1]);
28357
+ }
28358
+ }
28359
+ }
28360
+ }
28361
+ if (cur) fns.push(cur);
28362
+ for (const fn of fns) {
28363
+ for (let pass = 0; pass < 3; pass++) {
28364
+ const before = fn.tainted.size;
28365
+ for (let i2 = fn.start; i2 <= fn.end; i2++) {
28366
+ const t = lines[i2].trim();
28367
+ const m = t.match(/^let\s+(?:mut\s+)?(\w+)\s*(?::\s*[^=]+)?=\s*(.+?);?$/);
28368
+ if (!m) continue;
28369
+ const lhs = m[1];
28370
+ const rhs = m[2];
28371
+ if (fn.tainted.has(lhs)) continue;
28372
+ for (const v of fn.tainted) {
28373
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
28374
+ fn.tainted.add(lhs);
28375
+ break;
28376
+ }
28377
+ }
28378
+ }
28379
+ if (fn.tainted.size === before) break;
28380
+ }
28381
+ }
28382
+ const macroRe = /\b(info|warn|error|debug|trace)\s*!\s*\(\s*([^;]+?)\)\s*;?\s*$/;
28383
+ const seen = /* @__PURE__ */ new Set();
28384
+ for (const fn of fns) {
28385
+ if (fn.tainted.size === 0) continue;
28386
+ for (let i2 = fn.start; i2 <= fn.end; i2++) {
28387
+ const line = lines[i2];
28388
+ const m = line.match(macroRe);
28389
+ if (!m) continue;
28390
+ const macroName = m[1];
28391
+ const argSpan = m[2];
28392
+ const parts2 = [];
28393
+ {
28394
+ let d = 0;
28395
+ let b = "";
28396
+ let inStr = null;
28397
+ for (let k = 0; k < argSpan.length; k++) {
28398
+ const ch = argSpan[k];
28399
+ if (inStr) {
28400
+ if (ch === "\\") {
28401
+ b += ch + (argSpan[k + 1] ?? "");
28402
+ k++;
28403
+ continue;
28404
+ }
28405
+ if (ch === inStr) inStr = null;
28406
+ b += ch;
28407
+ continue;
28408
+ }
28409
+ if (ch === '"') {
28410
+ inStr = ch;
28411
+ b += ch;
28412
+ continue;
28413
+ }
28414
+ if (ch === "(" || ch === "[" || ch === "{") d++;
28415
+ else if (ch === ")" || ch === "]" || ch === "}") d--;
28416
+ if (ch === "," && d === 0) {
28417
+ parts2.push(b);
28418
+ b = "";
28419
+ continue;
28420
+ }
28421
+ b += ch;
28422
+ }
28423
+ if (b.trim().length > 0) parts2.push(b);
28424
+ }
28425
+ if (parts2.length < 2) continue;
28426
+ let tainted = false;
28427
+ for (let p = 1; p < parts2.length; p++) {
28428
+ for (const v of fn.tainted) {
28429
+ if (new RegExp(`\\b${v}\\b`).test(parts2[p])) {
28430
+ tainted = true;
28431
+ break;
28432
+ }
28433
+ }
28434
+ if (tainted) break;
28435
+ }
28436
+ if (!tainted) continue;
28437
+ const key = `${i2 + 1}:${macroName}`;
28438
+ if (seen.has(key)) continue;
28439
+ seen.add(key);
28440
+ findings.push({
28441
+ id: `log_injection-${file}-${i2 + 1}-rust-${macroName}`,
28442
+ pass: "language-sources",
28443
+ category: "security",
28444
+ rule_id: "log_injection",
28445
+ cwe: "CWE-117",
28446
+ severity: "medium",
28447
+ level: "warning",
28448
+ message: `Log injection: Rust \`${macroName}!(...)\` interpolates a tainted value into the log line. Unsanitized CRLF lets an attacker forge log entries or split records. Strip control characters or use a structured logging API.`,
28449
+ file,
28450
+ line: i2 + 1,
28451
+ snippet: line.trim()
28452
+ });
28453
+ }
28454
+ }
28455
+ return findings;
28456
+ }
27689
28457
 
27690
28458
  // src/analysis/passes/sink-filter-pass.ts
27691
28459
  var JS_XSS_SANITIZERS = [