circle-ir 3.210.0 → 3.213.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.
@@ -13156,9 +13156,16 @@ var DEFAULT_SINKS = [
13156
13156
  // HTML sanitizer bypass markers (known CVE patterns)
13157
13157
  { method: "clean", class: "AntiSamy", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
13158
13158
  { method: "getValidSafeHTML", class: "ESAPI", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
13159
- // Request/session attribute reflection XSS (return value is tainted)
13160
- { method: "getAttribute", class: "HttpServletRequest", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [] },
13161
- { method: "getAttribute", class: "HttpSession", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [] },
13159
+ // NOTE (cognium-dev#270): `HttpServletRequest`/`HttpSession.getAttribute()`
13160
+ // were registered here as xss sinks to model "attribute reflection XSS", but
13161
+ // getAttribute is a *read* its return value is tainted (a SOURCE, already
13162
+ // registered as `io_input` above), never an HTML-output context. With
13163
+ // `arg_positions: []` the entry produced a degenerate source==sink self-flow
13164
+ // (the same getAttribute call flagged as both), firing CWE-79 on route-helper
13165
+ // reads that are never rendered (elide `getPath`). The real sink is wherever
13166
+ // the read value is later written to a response (getWriter/JspWriter/template),
13167
+ // which the io_input source reaches on its own. Same over-broad-sink family as
13168
+ // #268 (StringBuilder.append). Removed.
13162
13169
  // =========================================================================
13163
13170
  // Rust Sinks
13164
13171
  // =========================================================================
@@ -16203,6 +16210,70 @@ function isNonExecutablePython(trimmed) {
16203
16210
  return false;
16204
16211
  }
16205
16212
 
16213
+ // src/analysis/sanitizer-index.ts
16214
+ var SANITIZER_SET_CACHE = /* @__PURE__ */ new WeakMap();
16215
+ function getSanitizesSet(san) {
16216
+ let s = SANITIZER_SET_CACHE.get(san);
16217
+ if (!s) {
16218
+ s = new Set(san.sanitizes);
16219
+ SANITIZER_SET_CACHE.set(san, s);
16220
+ }
16221
+ return s;
16222
+ }
16223
+ function sanitizerCoversSink(san, sinkType) {
16224
+ return getSanitizesSet(san).has(sinkType);
16225
+ }
16226
+
16227
+ // src/analysis/dfg-walk.ts
16228
+ var walkBackwardDefsMemo = /* @__PURE__ */ new WeakMap();
16229
+ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
16230
+ const maxHops = options.maxHops ?? 32;
16231
+ let perFile = walkBackwardDefsMemo.get(chainsByToDef);
16232
+ if (perFile !== void 0) {
16233
+ const key = `${startDefId}|${maxHops}`;
16234
+ const hit = perFile.get(key);
16235
+ if (hit !== void 0) return hit;
16236
+ }
16237
+ const visited = /* @__PURE__ */ new Set();
16238
+ const lines = /* @__PURE__ */ new Set();
16239
+ let hopCapReached = false;
16240
+ const startDef = defById.get(startDefId);
16241
+ if (!startDef) {
16242
+ return { visited, lines, hopCapReached: false };
16243
+ }
16244
+ visited.add(startDefId);
16245
+ lines.add(startDef.line);
16246
+ const queue = [startDefId];
16247
+ let hops = 0;
16248
+ while (queue.length > 0) {
16249
+ if (hops >= maxHops) {
16250
+ hopCapReached = true;
16251
+ break;
16252
+ }
16253
+ hops++;
16254
+ const currentId = queue.shift();
16255
+ const incoming = chainsByToDef.get(currentId);
16256
+ if (!incoming) continue;
16257
+ for (const chain of incoming) {
16258
+ const fromId = chain.from_def;
16259
+ if (visited.has(fromId)) continue;
16260
+ visited.add(fromId);
16261
+ const fromDef = defById.get(fromId);
16262
+ if (fromDef) {
16263
+ lines.add(fromDef.line);
16264
+ }
16265
+ queue.push(fromId);
16266
+ }
16267
+ }
16268
+ const result = { visited, lines, hopCapReached };
16269
+ if (perFile === void 0) {
16270
+ perFile = /* @__PURE__ */ new Map();
16271
+ walkBackwardDefsMemo.set(chainsByToDef, perFile);
16272
+ }
16273
+ perFile.set(`${startDefId}|${maxHops}`, result);
16274
+ return result;
16275
+ }
16276
+
16206
16277
  // src/analysis/findings.ts
16207
16278
  function canSourceReachSink(sourceType, sinkType) {
16208
16279
  const sourceToSinkMapping = {
@@ -17568,70 +17639,6 @@ var AnalysisPipeline = class {
17568
17639
  }
17569
17640
  };
17570
17641
 
17571
- // src/analysis/dfg-walk.ts
17572
- var walkBackwardDefsMemo = /* @__PURE__ */ new WeakMap();
17573
- function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
17574
- const maxHops = options.maxHops ?? 32;
17575
- let perFile = walkBackwardDefsMemo.get(chainsByToDef);
17576
- if (perFile !== void 0) {
17577
- const key = `${startDefId}|${maxHops}`;
17578
- const hit = perFile.get(key);
17579
- if (hit !== void 0) return hit;
17580
- }
17581
- const visited = /* @__PURE__ */ new Set();
17582
- const lines = /* @__PURE__ */ new Set();
17583
- let hopCapReached = false;
17584
- const startDef = defById.get(startDefId);
17585
- if (!startDef) {
17586
- return { visited, lines, hopCapReached: false };
17587
- }
17588
- visited.add(startDefId);
17589
- lines.add(startDef.line);
17590
- const queue = [startDefId];
17591
- let hops = 0;
17592
- while (queue.length > 0) {
17593
- if (hops >= maxHops) {
17594
- hopCapReached = true;
17595
- break;
17596
- }
17597
- hops++;
17598
- const currentId = queue.shift();
17599
- const incoming = chainsByToDef.get(currentId);
17600
- if (!incoming) continue;
17601
- for (const chain of incoming) {
17602
- const fromId = chain.from_def;
17603
- if (visited.has(fromId)) continue;
17604
- visited.add(fromId);
17605
- const fromDef = defById.get(fromId);
17606
- if (fromDef) {
17607
- lines.add(fromDef.line);
17608
- }
17609
- queue.push(fromId);
17610
- }
17611
- }
17612
- const result = { visited, lines, hopCapReached };
17613
- if (perFile === void 0) {
17614
- perFile = /* @__PURE__ */ new Map();
17615
- walkBackwardDefsMemo.set(chainsByToDef, perFile);
17616
- }
17617
- perFile.set(`${startDefId}|${maxHops}`, result);
17618
- return result;
17619
- }
17620
-
17621
- // src/analysis/sanitizer-index.ts
17622
- var SANITIZER_SET_CACHE = /* @__PURE__ */ new WeakMap();
17623
- function getSanitizesSet(san) {
17624
- let s = SANITIZER_SET_CACHE.get(san);
17625
- if (!s) {
17626
- s = new Set(san.sanitizes);
17627
- SANITIZER_SET_CACHE.set(san, s);
17628
- }
17629
- return s;
17630
- }
17631
- function sanitizerCoversSink(san, sinkType) {
17632
- return getSanitizesSet(san).has(sinkType);
17633
- }
17634
-
17635
17642
  // src/analysis/taint-propagation.ts
17636
17643
  function buildSanitizersByLine(sanitizers) {
17637
17644
  const out2 = /* @__PURE__ */ new Map();
@@ -26168,6 +26175,7 @@ var LanguageSourcesPass = class {
26168
26175
  ...findJavaPathNormalizeStartsWithGuardSanitizers(code)
26169
26176
  );
26170
26177
  additionalSanitizers.push(...findJavaPathGetFileNameSanitizers(code));
26178
+ additionalSanitizers.push(...findJavaCanonicalPathStartsWithGuardSanitizers(code));
26171
26179
  additionalSanitizers.push(...findJavaInlineCrlfStripLogSanitizers(code));
26172
26180
  additionalSanitizers.push(...findJavaArgvFormExecSanitizers(code));
26173
26181
  for (const finding of findJavaPatternFindings(code, graph.ir.meta.file)) {
@@ -28719,19 +28727,65 @@ function findJavaPathNormalizeStartsWithGuardSanitizers(code) {
28719
28727
  }
28720
28728
  return sanitizers;
28721
28729
  }
28730
+ function findJavaCanonicalPathStartsWithGuardSanitizers(code) {
28731
+ const sanitizers = [];
28732
+ const lines = code.split("\n");
28733
+ const guardRe = /\bif\s*\(\s*!\s*([A-Za-z_]\w*)\s*\.\s*getCanonicalPath\s*\(\s*\)\s*\.\s*startsWith\s*\(/;
28734
+ const terminatorRe = /\b(throw|return)\b/;
28735
+ const assignRe = /^\s*(?:[A-Za-z_][\w.<>[\]]*\s+)?([A-Za-z_]\w*)\s*=(?!=)\s*(.*)$/;
28736
+ for (let i2 = 0; i2 < lines.length; i2++) {
28737
+ const m = guardRe.exec(lines[i2]);
28738
+ if (!m) continue;
28739
+ let hasTerminator = terminatorRe.test(lines[i2]);
28740
+ if (!hasTerminator) {
28741
+ for (let j = i2 + 1; j < Math.min(lines.length, i2 + 6); j++) {
28742
+ if (terminatorRe.test(lines[j])) {
28743
+ hasTerminator = true;
28744
+ break;
28745
+ }
28746
+ if (lines[j].includes("}")) break;
28747
+ }
28748
+ }
28749
+ if (!hasTerminator) continue;
28750
+ const guarded = /* @__PURE__ */ new Set([m[1]]);
28751
+ const mentions = (t) => {
28752
+ for (const n of guarded) if (new RegExp(`\\b${n}\\b`).test(t)) return true;
28753
+ return false;
28754
+ };
28755
+ for (let l = 0; l < lines.length; l++) {
28756
+ const lt = lines[l];
28757
+ const a = assignRe.exec(lt);
28758
+ if (a) {
28759
+ if (mentions(a[2])) guarded.add(a[1]);
28760
+ else if (a[1] !== m[1]) guarded.delete(a[1]);
28761
+ }
28762
+ if (mentions(lt)) {
28763
+ sanitizers.push({
28764
+ type: "java_canonical_startswith_guard",
28765
+ method: "getCanonicalPath",
28766
+ line: l + 1,
28767
+ sanitizes: ["path_traversal", "external_taint_escape"]
28768
+ });
28769
+ }
28770
+ }
28771
+ }
28772
+ return sanitizers;
28773
+ }
28722
28774
  function findJavaPathGetFileNameSanitizers(code) {
28723
28775
  const sanitizers = [];
28724
28776
  const lines = code.split("\n");
28725
28777
  const assignmentRe2 = /^\s*(?:(?:final\s+)?[A-Za-z_][\w.<>?,\s\[\]]*?\s+)?([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
28726
28778
  const rhsHasGetFileNameRe = /\.\s*getFileName\s*\(\s*\)/;
28727
28779
  const rhsHasPathsChainRe = /\b(?:Paths\s*\.\s*get|Path\s*\.\s*of)\s*\(/;
28780
+ const rhsNewFileGetNameRe = /\bnew\s+File\s*\([\s\S]*\)\s*\.\s*getName\s*\(\s*\)/;
28728
28781
  const candidates = [];
28729
28782
  for (let i2 = 0; i2 < lines.length; i2++) {
28730
28783
  const m = assignmentRe2.exec(lines[i2]);
28731
28784
  if (!m) continue;
28732
28785
  const rhs = m[2];
28733
- if (!rhsHasGetFileNameRe.test(rhs)) continue;
28734
- if (!rhsHasPathsChainRe.test(rhs)) continue;
28786
+ const isNioGetFileName = rhsHasGetFileNameRe.test(rhs) && rhsHasPathsChainRe.test(rhs);
28787
+ const isNewFileGetName = rhsNewFileGetNameRe.test(rhs);
28788
+ if (!isNioGetFileName && !isNewFileGetName) continue;
28735
28789
  candidates.push({ line: i2 + 1, boundVar: m[1] });
28736
28790
  }
28737
28791
  if (candidates.length === 0) return sanitizers;
@@ -12551,9 +12551,16 @@ var DEFAULT_SINKS = [
12551
12551
  // HTML sanitizer bypass markers (known CVE patterns)
12552
12552
  { method: "clean", class: "AntiSamy", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
12553
12553
  { method: "getValidSafeHTML", class: "ESAPI", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
12554
- // Request/session attribute reflection XSS (return value is tainted)
12555
- { method: "getAttribute", class: "HttpServletRequest", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [] },
12556
- { method: "getAttribute", class: "HttpSession", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [] },
12554
+ // NOTE (cognium-dev#270): `HttpServletRequest`/`HttpSession.getAttribute()`
12555
+ // were registered here as xss sinks to model "attribute reflection XSS", but
12556
+ // getAttribute is a *read* its return value is tainted (a SOURCE, already
12557
+ // registered as `io_input` above), never an HTML-output context. With
12558
+ // `arg_positions: []` the entry produced a degenerate source==sink self-flow
12559
+ // (the same getAttribute call flagged as both), firing CWE-79 on route-helper
12560
+ // reads that are never rendered (elide `getPath`). The real sink is wherever
12561
+ // the read value is later written to a response (getWriter/JspWriter/template),
12562
+ // which the io_input source reaches on its own. Same over-broad-sink family as
12563
+ // #268 (StringBuilder.append). Removed.
12557
12564
  // =========================================================================
12558
12565
  // Rust Sinks
12559
12566
  // =========================================================================
@@ -12485,9 +12485,16 @@ var DEFAULT_SINKS = [
12485
12485
  // HTML sanitizer bypass markers (known CVE patterns)
12486
12486
  { method: "clean", class: "AntiSamy", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
12487
12487
  { method: "getValidSafeHTML", class: "ESAPI", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [0] },
12488
- // Request/session attribute reflection XSS (return value is tainted)
12489
- { method: "getAttribute", class: "HttpServletRequest", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [] },
12490
- { method: "getAttribute", class: "HttpSession", type: "xss", cwe: "CWE-79", severity: "medium", arg_positions: [] },
12488
+ // NOTE (cognium-dev#270): `HttpServletRequest`/`HttpSession.getAttribute()`
12489
+ // were registered here as xss sinks to model "attribute reflection XSS", but
12490
+ // getAttribute is a *read* its return value is tainted (a SOURCE, already
12491
+ // registered as `io_input` above), never an HTML-output context. With
12492
+ // `arg_positions: []` the entry produced a degenerate source==sink self-flow
12493
+ // (the same getAttribute call flagged as both), firing CWE-79 on route-helper
12494
+ // reads that are never rendered (elide `getPath`). The real sink is wherever
12495
+ // the read value is later written to a response (getWriter/JspWriter/template),
12496
+ // which the io_input source reaches on its own. Same over-broad-sink family as
12497
+ // #268 (StringBuilder.append). Removed.
12491
12498
  // =========================================================================
12492
12499
  // Rust Sinks
12493
12500
  // =========================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.210.0",
3
+ "version": "3.213.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",