circle-ir 3.134.0 → 3.136.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.
@@ -23720,6 +23720,18 @@ var LanguageSourcesPass = class {
23720
23720
  )) {
23721
23721
  ctx.addFinding(finding);
23722
23722
  }
23723
+ for (const finding of findPythonTaintedFormatStringFindings(
23724
+ code,
23725
+ graph.ir.meta.file
23726
+ )) {
23727
+ ctx.addFinding(finding);
23728
+ }
23729
+ for (const finding of findPythonHeaderCrlfInjectionFindings(
23730
+ code,
23731
+ graph.ir.meta.file
23732
+ )) {
23733
+ ctx.addFinding(finding);
23734
+ }
23723
23735
  }
23724
23736
  if (language === "rust") {
23725
23737
  additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
@@ -23783,6 +23795,12 @@ var LanguageSourcesPass = class {
23783
23795
  )) {
23784
23796
  ctx.addFinding(finding);
23785
23797
  }
23798
+ for (const finding of findJsUtilFormatFormatStringFindings(
23799
+ code,
23800
+ graph.ir.meta.file
23801
+ )) {
23802
+ ctx.addFinding(finding);
23803
+ }
23786
23804
  }
23787
23805
  if (language === "java") {
23788
23806
  additionalSanitizers.push(...findJavaSafeJsonParseSanitizers(code));
@@ -23803,6 +23821,12 @@ var LanguageSourcesPass = class {
23803
23821
  )) {
23804
23822
  ctx.addFinding(finding);
23805
23823
  }
23824
+ for (const finding of findJavaUrlOpenStreamSsrfFindings(
23825
+ code,
23826
+ graph.ir.meta.file
23827
+ )) {
23828
+ ctx.addFinding(finding);
23829
+ }
23806
23830
  }
23807
23831
  if (language === "python" || language === "javascript" || language === "typescript" || language === "go") {
23808
23832
  const exfilFindings = findExternalSecretExfiltrationFindings(
@@ -27606,6 +27630,299 @@ function findPythonMongoengineWhereNosqlInjectionFindings(code, file) {
27606
27630
  }
27607
27631
  return findings;
27608
27632
  }
27633
+ function findJavaUrlOpenStreamSsrfFindings(code, file) {
27634
+ const findings = [];
27635
+ if (typeof code !== "string" || code.length === 0) return findings;
27636
+ if (!/\bnew\s+URL\s*\(/.test(code) && !/\bURI\s*\.\s*create\s*\(/.test(code)) {
27637
+ return findings;
27638
+ }
27639
+ if (!/\.\s*(?:openStream|openConnection|getContent)\s*\(/.test(code)) {
27640
+ return findings;
27641
+ }
27642
+ const lines = code.split("\n");
27643
+ const reqExtractRe = /\b\w+\s*\.\s*(?:getParameter|getParameterValues|getHeader|getHeaders|getCookies|getReader|getQueryString|getRequestURI|getInputStream|getPart|getParts)\s*\(/;
27644
+ const taintedVars = /* @__PURE__ */ new Set();
27645
+ for (let pass = 0; pass < 3; pass++) {
27646
+ const before = taintedVars.size;
27647
+ for (let i2 = 0; i2 < lines.length; i2++) {
27648
+ const t = lines[i2].trim();
27649
+ const a = t.match(/^(?:final\s+)?(?:[\w<>?,\[\]]+\s+)?(\w+)\s*=\s*(.+?);?$/);
27650
+ if (!a) continue;
27651
+ const lhs = a[1];
27652
+ const rhs = a[2];
27653
+ if (taintedVars.has(lhs)) continue;
27654
+ if (reqExtractRe.test(rhs)) {
27655
+ taintedVars.add(lhs);
27656
+ continue;
27657
+ }
27658
+ for (const v of taintedVars) {
27659
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27660
+ taintedVars.add(lhs);
27661
+ break;
27662
+ }
27663
+ }
27664
+ }
27665
+ if (taintedVars.size === before) break;
27666
+ }
27667
+ if (taintedVars.size === 0) return findings;
27668
+ const sinkRe = /\.\s*(openStream|openConnection|getContent)\s*\(/;
27669
+ const seen = /* @__PURE__ */ new Set();
27670
+ for (let i2 = 0; i2 < lines.length; i2++) {
27671
+ const line = lines[i2];
27672
+ const m = line.match(sinkRe);
27673
+ if (!m) continue;
27674
+ const op = m[1];
27675
+ const sinkIdx = line.indexOf(`.${op}`);
27676
+ if (sinkIdx < 0) continue;
27677
+ const head = line.substring(0, sinkIdx + 1);
27678
+ let tainted = reqExtractRe.test(head);
27679
+ if (!tainted) {
27680
+ for (const v of taintedVars) {
27681
+ if (new RegExp(`\\b${v}\\b`).test(head)) {
27682
+ tainted = true;
27683
+ break;
27684
+ }
27685
+ }
27686
+ }
27687
+ if (!tainted) continue;
27688
+ const key = `${i2 + 1}:${op}`;
27689
+ if (seen.has(key)) continue;
27690
+ seen.add(key);
27691
+ findings.push({
27692
+ id: `ssrf-${file}-${i2 + 1}-java-url-${op.toLowerCase()}`,
27693
+ pass: "language-sources",
27694
+ category: "security",
27695
+ rule_id: "ssrf",
27696
+ cwe: "CWE-918",
27697
+ severity: "critical",
27698
+ level: "error",
27699
+ message: `SSRF: Java \`URL.${op}()\` invoked on a URL derived from servlet request input. Even when an \`if (url.startsWith("https://"))\` guard is present, the host portion remains attacker-controlled \u2014 use a strict host allowlist (parse the URL, check \`getHost()\`) before issuing the request.`,
27700
+ file,
27701
+ line: i2 + 1,
27702
+ snippet: line.trim()
27703
+ });
27704
+ }
27705
+ return findings;
27706
+ }
27707
+ function findPythonTaintedFormatStringFindings(code, file) {
27708
+ const findings = [];
27709
+ if (typeof code !== "string" || code.length === 0) return findings;
27710
+ if (!/\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)|flask\.request)\b/.test(
27711
+ code
27712
+ )) {
27713
+ return findings;
27714
+ }
27715
+ const lines = code.split("\n");
27716
+ const reqExtractRe = /\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)\b|flask\.request\b)/;
27717
+ const taintedVars = /* @__PURE__ */ new Set();
27718
+ for (let pass = 0; pass < 3; pass++) {
27719
+ const before = taintedVars.size;
27720
+ for (let i2 = 0; i2 < lines.length; i2++) {
27721
+ const t = lines[i2].trim();
27722
+ if (t.startsWith("#")) continue;
27723
+ const a = t.match(/^(\w+)\s*=\s*(.+?)$/);
27724
+ if (!a) continue;
27725
+ const lhs = a[1];
27726
+ const rhs = a[2];
27727
+ if (taintedVars.has(lhs)) continue;
27728
+ if (reqExtractRe.test(rhs)) {
27729
+ taintedVars.add(lhs);
27730
+ continue;
27731
+ }
27732
+ for (const v of taintedVars) {
27733
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27734
+ taintedVars.add(lhs);
27735
+ break;
27736
+ }
27737
+ }
27738
+ }
27739
+ if (taintedVars.size === before) break;
27740
+ }
27741
+ if (taintedVars.size === 0) return findings;
27742
+ const percentRe = /\b(\w+)\s*%\s*[\(\[\{"'\w]/;
27743
+ const dotFormatRe = /\b(\w+)\s*\.\s*format\s*\(/;
27744
+ const seen = /* @__PURE__ */ new Set();
27745
+ for (let i2 = 0; i2 < lines.length; i2++) {
27746
+ const line = lines[i2];
27747
+ const t = line.trim();
27748
+ if (t.startsWith("#")) continue;
27749
+ const pm = line.match(percentRe);
27750
+ if (pm && taintedVars.has(pm[1])) {
27751
+ const key = `${i2 + 1}:percent`;
27752
+ if (!seen.has(key)) {
27753
+ seen.add(key);
27754
+ findings.push({
27755
+ id: `format_string-${file}-${i2 + 1}-py-percent`,
27756
+ pass: "language-sources",
27757
+ category: "security",
27758
+ rule_id: "format_string",
27759
+ cwe: "CWE-134",
27760
+ severity: "high",
27761
+ level: "error",
27762
+ 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.",
27763
+ file,
27764
+ line: i2 + 1,
27765
+ snippet: line.trim()
27766
+ });
27767
+ }
27768
+ }
27769
+ const dm = line.match(dotFormatRe);
27770
+ if (dm && taintedVars.has(dm[1])) {
27771
+ const key = `${i2 + 1}:dotformat`;
27772
+ if (!seen.has(key)) {
27773
+ seen.add(key);
27774
+ findings.push({
27775
+ id: `format_string-${file}-${i2 + 1}-py-strformat`,
27776
+ pass: "language-sources",
27777
+ category: "security",
27778
+ rule_id: "format_string",
27779
+ cwe: "CWE-134",
27780
+ severity: "high",
27781
+ level: "error",
27782
+ 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.",
27783
+ file,
27784
+ line: i2 + 1,
27785
+ snippet: line.trim()
27786
+ });
27787
+ }
27788
+ }
27789
+ }
27790
+ return findings;
27791
+ }
27792
+ function findJsUtilFormatFormatStringFindings(code, file) {
27793
+ const findings = [];
27794
+ if (typeof code !== "string" || code.length === 0) return findings;
27795
+ if (!/\butil\s*\.\s*format\s*\(/.test(code)) return findings;
27796
+ if (!/\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/.test(code)) {
27797
+ return findings;
27798
+ }
27799
+ const lines = code.split("\n");
27800
+ const reqExtractRe = /\breq\s*\.\s*(?:query|body|params|headers|cookies)\b/;
27801
+ const taintedVars = /* @__PURE__ */ new Set();
27802
+ for (let pass = 0; pass < 3; pass++) {
27803
+ const before = taintedVars.size;
27804
+ for (let i2 = 0; i2 < lines.length; i2++) {
27805
+ const t = lines[i2].trim();
27806
+ if (t.startsWith("//")) continue;
27807
+ const a = t.match(/^(?:const|let|var)\s+(\w+)\s*=\s*(.+?);?$/);
27808
+ if (!a) continue;
27809
+ const lhs = a[1];
27810
+ const rhs = a[2];
27811
+ if (taintedVars.has(lhs)) continue;
27812
+ if (reqExtractRe.test(rhs)) {
27813
+ taintedVars.add(lhs);
27814
+ continue;
27815
+ }
27816
+ for (const v of taintedVars) {
27817
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27818
+ taintedVars.add(lhs);
27819
+ break;
27820
+ }
27821
+ }
27822
+ }
27823
+ if (taintedVars.size === before) break;
27824
+ }
27825
+ if (taintedVars.size === 0) return findings;
27826
+ const callRe = /\butil\s*\.\s*format\s*\(\s*([\w.]+)\s*[,)]/;
27827
+ for (let i2 = 0; i2 < lines.length; i2++) {
27828
+ const line = lines[i2];
27829
+ const m = line.match(callRe);
27830
+ if (!m) continue;
27831
+ const arg = m[1];
27832
+ const root = arg.split(".")[0];
27833
+ if (!taintedVars.has(root) && !reqExtractRe.test(arg)) continue;
27834
+ findings.push({
27835
+ id: `format_string-${file}-${i2 + 1}-js-util-format`,
27836
+ pass: "language-sources",
27837
+ category: "security",
27838
+ rule_id: "format_string",
27839
+ cwe: "CWE-134",
27840
+ severity: "medium",
27841
+ level: "error",
27842
+ 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.",
27843
+ file,
27844
+ line: i2 + 1,
27845
+ snippet: line.trim()
27846
+ });
27847
+ }
27848
+ return findings;
27849
+ }
27850
+ function findPythonHeaderCrlfInjectionFindings(code, file) {
27851
+ const findings = [];
27852
+ if (typeof code !== "string" || code.length === 0) return findings;
27853
+ if (!/\b\w+\s*\.\s*headers\b/.test(code)) return findings;
27854
+ if (!/\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)|flask\.request)\b/.test(
27855
+ code
27856
+ )) {
27857
+ return findings;
27858
+ }
27859
+ const lines = code.split("\n");
27860
+ const reqExtractRe = /\b(?:request\.(?:args|form|values|json|files|cookies|headers|data)\b|flask\.request\b)/;
27861
+ const taintedVars = /* @__PURE__ */ new Set();
27862
+ for (let pass = 0; pass < 3; pass++) {
27863
+ const before = taintedVars.size;
27864
+ for (let i2 = 0; i2 < lines.length; i2++) {
27865
+ const t = lines[i2].trim();
27866
+ if (t.startsWith("#")) continue;
27867
+ const a = t.match(/^(\w+)\s*=\s*(.+?)$/);
27868
+ if (!a) continue;
27869
+ const lhs = a[1];
27870
+ const rhs = a[2];
27871
+ if (taintedVars.has(lhs)) continue;
27872
+ if (reqExtractRe.test(rhs)) {
27873
+ taintedVars.add(lhs);
27874
+ continue;
27875
+ }
27876
+ for (const v of taintedVars) {
27877
+ if (new RegExp(`\\b${v}\\b`).test(rhs)) {
27878
+ taintedVars.add(lhs);
27879
+ break;
27880
+ }
27881
+ }
27882
+ }
27883
+ if (taintedVars.size === before) break;
27884
+ }
27885
+ if (taintedVars.size === 0) return findings;
27886
+ const subscriptRe = /\b\w+\s*\.\s*headers\s*\[\s*['"][^'"]+['"]\s*\]\s*=\s*(.+)$/;
27887
+ const methodRe = /\b\w+\s*\.\s*headers\s*\.\s*(?:add|set|setdefault|append)\s*\(\s*['"][^'"]+['"]\s*,\s*(.+?)\)/;
27888
+ for (let i2 = 0; i2 < lines.length; i2++) {
27889
+ const line = lines[i2];
27890
+ const t = line.trim();
27891
+ if (t.startsWith("#")) continue;
27892
+ let expr = null;
27893
+ const sm = line.match(subscriptRe);
27894
+ if (sm) expr = sm[1];
27895
+ else {
27896
+ const mm = line.match(methodRe);
27897
+ if (mm) expr = mm[1];
27898
+ }
27899
+ if (!expr) continue;
27900
+ let tainted = reqExtractRe.test(expr);
27901
+ if (!tainted) {
27902
+ for (const v of taintedVars) {
27903
+ if (new RegExp(`\\b${v}\\b`).test(expr)) {
27904
+ tainted = true;
27905
+ break;
27906
+ }
27907
+ }
27908
+ }
27909
+ if (!tainted) continue;
27910
+ findings.push({
27911
+ id: `crlf-${file}-${i2 + 1}-py-headers`,
27912
+ pass: "language-sources",
27913
+ category: "security",
27914
+ rule_id: "crlf",
27915
+ cwe: "CWE-113",
27916
+ severity: "medium",
27917
+ level: "error",
27918
+ 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.",
27919
+ file,
27920
+ line: i2 + 1,
27921
+ snippet: line.trim()
27922
+ });
27923
+ }
27924
+ return findings;
27925
+ }
27609
27926
 
27610
27927
  // src/analysis/passes/sink-filter-pass.ts
27611
27928
  var JS_XSS_SANITIZERS = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.134.0",
3
+ "version": "3.136.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",