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.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +10 -3
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/findings.d.ts +2 -2
- package/dist/analysis/findings.d.ts.map +1 -1
- package/dist/analysis/findings.js +77 -1
- package/dist/analysis/findings.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +83 -4
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/browser/circle-ir.js +123 -69
- package/dist/core/circle-ir-core.cjs +10 -3
- package/dist/core/circle-ir-core.js +10 -3
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
13160
|
-
|
|
13161
|
-
|
|
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
|
-
|
|
28734
|
-
|
|
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
|
-
//
|
|
12555
|
-
|
|
12556
|
-
|
|
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
|
-
//
|
|
12489
|
-
|
|
12490
|
-
|
|
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.
|
|
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",
|