circle-ir 3.144.0 → 3.145.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/entry-point-detection.d.ts.map +1 -1
- package/dist/analysis/entry-point-detection.js +31 -0
- package/dist/analysis/entry-point-detection.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts +19 -0
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +133 -3
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +31 -1
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +45 -3
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +158 -7
- package/dist/core/circle-ir-core.cjs +27 -4
- package/dist/core/circle-ir-core.js +27 -4
- package/package.json +1 -1
|
@@ -13269,7 +13269,20 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
13269
13269
|
line: paramLine,
|
|
13270
13270
|
confidence: param.type ? 0.7 : 0.5,
|
|
13271
13271
|
// Lower confidence for untyped params
|
|
13272
|
-
in_method: method.name
|
|
13272
|
+
in_method: method.name,
|
|
13273
|
+
// cognium-dev #220 — expose the parameter name to variable-scan flow
|
|
13274
|
+
// detection so uses of the param (including through concat-derived
|
|
13275
|
+
// aliases via `buildJavaTaintedVars`) can be bridged to sinks.
|
|
13276
|
+
//
|
|
13277
|
+
// Java-only: the Python/Rust alias-expansion branches in
|
|
13278
|
+
// taint-propagation-pass.ts use the earliest sourcesWithVar entry
|
|
13279
|
+
// as the anchor for synthetic derived sources. Adding
|
|
13280
|
+
// interprocedural_param to sourcesWithVar in those languages
|
|
13281
|
+
// would cause the anchor to become the low-confidence
|
|
13282
|
+
// interprocedural_param at the method decl line instead of the
|
|
13283
|
+
// real HTTP source, breaking downstream sink-type filters
|
|
13284
|
+
// (regressed #78, #92.1, #105 FP-31, #215 recall).
|
|
13285
|
+
...language === "java" ? { variable: param.name } : {}
|
|
13273
13286
|
});
|
|
13274
13287
|
}
|
|
13275
13288
|
}
|
|
@@ -13733,7 +13746,16 @@ function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
|
|
|
13733
13746
|
return false;
|
|
13734
13747
|
}
|
|
13735
13748
|
var TEMPLATE_LITERAL_RECEIVER_RE = /^Template\(\s*(?:"[^"\\]*"|'[^'\\]*')\s*\)$/;
|
|
13736
|
-
|
|
13749
|
+
var JINJA_AUTOESCAPE_TRUE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*True\b/;
|
|
13750
|
+
var JINJA_SELECT_AUTOESCAPE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*select_autoescape\s*\(/;
|
|
13751
|
+
var JINJA_AUTOESCAPE_FALSE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False\b/;
|
|
13752
|
+
function fileHasSafeJinjaEnvironment(sourceLines) {
|
|
13753
|
+
if (!sourceLines || sourceLines.length === 0) return false;
|
|
13754
|
+
const text = sourceLines.join("\n");
|
|
13755
|
+
if (JINJA_AUTOESCAPE_FALSE_RE.test(text)) return false;
|
|
13756
|
+
return JINJA_AUTOESCAPE_TRUE_RE.test(text) || JINJA_SELECT_AUTOESCAPE_RE.test(text);
|
|
13757
|
+
}
|
|
13758
|
+
function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
13737
13759
|
if (language !== "python") return false;
|
|
13738
13760
|
if (pattern.type !== "xss" && pattern.type !== "code_injection") return false;
|
|
13739
13761
|
const method = call.method_name;
|
|
@@ -13747,7 +13769,8 @@ function isSafeJinjaRenderCall(call, pattern, language) {
|
|
|
13747
13769
|
}
|
|
13748
13770
|
if (method === "render") {
|
|
13749
13771
|
const receiver = (call.receiver ?? "").trim();
|
|
13750
|
-
|
|
13772
|
+
if (TEMPLATE_LITERAL_RECEIVER_RE.test(receiver)) return true;
|
|
13773
|
+
if (fileHasSafeJinjaEnvironment(sourceLines)) return true;
|
|
13751
13774
|
}
|
|
13752
13775
|
return false;
|
|
13753
13776
|
}
|
|
@@ -13795,7 +13818,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13795
13818
|
if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
|
|
13796
13819
|
continue;
|
|
13797
13820
|
}
|
|
13798
|
-
if (isSafeJinjaRenderCall(call, pattern, language)) {
|
|
13821
|
+
if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
|
|
13799
13822
|
continue;
|
|
13800
13823
|
}
|
|
13801
13824
|
const location = formatCallLocation(call);
|
|
@@ -24734,6 +24757,65 @@ function buildRustTaintedVars(sourceCode, seedVars) {
|
|
|
24734
24757
|
}
|
|
24735
24758
|
return derived;
|
|
24736
24759
|
}
|
|
24760
|
+
function buildJavaTaintedVars(sourceCode, seedVars) {
|
|
24761
|
+
const derived = /* @__PURE__ */ new Map();
|
|
24762
|
+
const knownTainted = new Set(seedVars);
|
|
24763
|
+
const lines = sourceCode.split("\n");
|
|
24764
|
+
const declRe = /^\s*(?:public|private|protected|static|final|volatile|transient|\s)*\s*(?:[A-Za-z_][\w.]*(?:\s*<[^>]*>)?(?:\s*\[\s*\])*)\s+([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
24765
|
+
const assignRe = /^\s*([A-Za-z_]\w*)\s*=\s*(.+?);\s*$/;
|
|
24766
|
+
const JAVA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
24767
|
+
"if",
|
|
24768
|
+
"else",
|
|
24769
|
+
"while",
|
|
24770
|
+
"for",
|
|
24771
|
+
"do",
|
|
24772
|
+
"switch",
|
|
24773
|
+
"case",
|
|
24774
|
+
"return",
|
|
24775
|
+
"throw",
|
|
24776
|
+
"try",
|
|
24777
|
+
"catch",
|
|
24778
|
+
"finally",
|
|
24779
|
+
"new",
|
|
24780
|
+
"this",
|
|
24781
|
+
"super",
|
|
24782
|
+
"break",
|
|
24783
|
+
"continue",
|
|
24784
|
+
"default",
|
|
24785
|
+
"class",
|
|
24786
|
+
"interface",
|
|
24787
|
+
"enum"
|
|
24788
|
+
]);
|
|
24789
|
+
let changed = true;
|
|
24790
|
+
let guard = 0;
|
|
24791
|
+
while (changed && guard < lines.length + 2) {
|
|
24792
|
+
changed = false;
|
|
24793
|
+
guard++;
|
|
24794
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
24795
|
+
const line = lines[i2];
|
|
24796
|
+
const trimmed = line.trimStart();
|
|
24797
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
24798
|
+
const declMatch = declRe.exec(line);
|
|
24799
|
+
const assignMatch = !declMatch ? assignRe.exec(line) : null;
|
|
24800
|
+
const m = declMatch ?? assignMatch;
|
|
24801
|
+
if (!m) continue;
|
|
24802
|
+
const lhs = m[1];
|
|
24803
|
+
const rhs = m[2];
|
|
24804
|
+
if (JAVA_KEYWORDS.has(lhs)) continue;
|
|
24805
|
+
if (knownTainted.has(lhs)) continue;
|
|
24806
|
+
const escaped = (v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
24807
|
+
const ref = [...knownTainted].some(
|
|
24808
|
+
(v) => new RegExp(`(?<![\\p{L}\\p{N}_])${escaped(v)}(?![\\p{L}\\p{N}_])`, "u").test(rhs)
|
|
24809
|
+
);
|
|
24810
|
+
if (ref) {
|
|
24811
|
+
derived.set(lhs, i2 + 1);
|
|
24812
|
+
knownTainted.add(lhs);
|
|
24813
|
+
changed = true;
|
|
24814
|
+
}
|
|
24815
|
+
}
|
|
24816
|
+
}
|
|
24817
|
+
return derived;
|
|
24818
|
+
}
|
|
24737
24819
|
var BASH_UNTRUSTED_ENV_PATTERNS = [
|
|
24738
24820
|
/^USER_INPUT$/i,
|
|
24739
24821
|
/^QUERY_STRING$/i,
|
|
@@ -25091,7 +25173,7 @@ function findBashRealpathPrefixGuardSanitizers(code) {
|
|
|
25091
25173
|
const caseOpen = /^\s*case\s+"?\$\{?\w+\}?"?\s+in\b/;
|
|
25092
25174
|
const esacClose = /^\s*esac\b/;
|
|
25093
25175
|
const armOpener = /^\s*([^)\s][^)]*?)\)/;
|
|
25094
|
-
const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|[\w\-./]+)(?:\/|\*)/;
|
|
25176
|
+
const prefixArm = /^(?:"\$\{?\w+\}?"|"[^"]*"|\/[\w\-./]+|\$\{?\w+\}?|https?:\/\/[\w\-.]+|[\w\-./]+)(?:\/|\*)/;
|
|
25095
25177
|
const catchAllArm = /^(?:\*|\\\*)$/;
|
|
25096
25178
|
let i2 = 0;
|
|
25097
25179
|
while (i2 < lines.length) {
|
|
@@ -25916,9 +25998,11 @@ function findJavaArgvFormExecSanitizers(code) {
|
|
|
25916
25998
|
const lines = code.split("\n");
|
|
25917
25999
|
const argvExecRe = /\.\s*exec\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
|
|
25918
26000
|
const argvPbRe = /\bnew\s+ProcessBuilder\s*\(\s*new\s+String\s*\[\s*\]\s*\{/;
|
|
26001
|
+
const shellInStringRe = /new\s+String\s*\[\s*\]\s*\{\s*"(?:\/(?:usr\/)?bin\/(?:sh|bash|zsh|ksh|dash)|(?:sh|bash|zsh|ksh|dash)|cmd(?:\.exe)?|powershell(?:\.exe)?|pwsh)"\s*,\s*"(?:-c|\/c|-Command|-command)"/i;
|
|
25919
26002
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
25920
26003
|
const text = lines[i2];
|
|
25921
26004
|
if (!argvExecRe.test(text) && !argvPbRe.test(text)) continue;
|
|
26005
|
+
if (shellInStringRe.test(text)) continue;
|
|
25922
26006
|
sanitizers.push({
|
|
25923
26007
|
type: "java_argv_form_exec",
|
|
25924
26008
|
method: "exec",
|
|
@@ -27855,6 +27939,22 @@ function findPythonMongoengineWhereNosqlInjectionFindings(code, file) {
|
|
|
27855
27939
|
}
|
|
27856
27940
|
return findings;
|
|
27857
27941
|
}
|
|
27942
|
+
function hasHostAllowlistBeforeSink(taintedVars, lines, sinkLineIdx) {
|
|
27943
|
+
for (let i2 = 0; i2 < sinkLineIdx; i2++) {
|
|
27944
|
+
const t = lines[i2];
|
|
27945
|
+
for (const v of taintedVars) {
|
|
27946
|
+
const containsRe = new RegExp(
|
|
27947
|
+
`\\.\\s*contains\\s*\\(\\s*${v}\\s*\\.\\s*getHost\\s*\\(\\s*\\)\\s*\\)`
|
|
27948
|
+
);
|
|
27949
|
+
if (containsRe.test(t)) return true;
|
|
27950
|
+
const equalsRe = new RegExp(
|
|
27951
|
+
`\\b${v}\\s*\\.\\s*getHost\\s*\\(\\s*\\)\\s*\\.\\s*equals(?:IgnoreCase)?\\s*\\(\\s*"[^"]+"\\s*\\)`
|
|
27952
|
+
);
|
|
27953
|
+
if (equalsRe.test(t)) return true;
|
|
27954
|
+
}
|
|
27955
|
+
}
|
|
27956
|
+
return false;
|
|
27957
|
+
}
|
|
27858
27958
|
function findJavaUrlOpenStreamSsrfFindings(code, file) {
|
|
27859
27959
|
const findings = [];
|
|
27860
27960
|
if (typeof code !== "string" || code.length === 0) return findings;
|
|
@@ -27910,6 +28010,7 @@ function findJavaUrlOpenStreamSsrfFindings(code, file) {
|
|
|
27910
28010
|
}
|
|
27911
28011
|
}
|
|
27912
28012
|
if (!tainted) continue;
|
|
28013
|
+
if (hasHostAllowlistBeforeSink(taintedVars, lines, i2)) continue;
|
|
27913
28014
|
const key = `${i2 + 1}:${op}`;
|
|
27914
28015
|
if (seen.has(key)) continue;
|
|
27915
28016
|
seen.add(key);
|
|
@@ -31090,6 +31191,25 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
|
|
|
31090
31191
|
}
|
|
31091
31192
|
}
|
|
31092
31193
|
}
|
|
31194
|
+
if (language === "java" && typeof code === "string" && sourcesWithVar.length > 0) {
|
|
31195
|
+
const seedVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
31196
|
+
const derived = buildJavaTaintedVars(code, seedVars);
|
|
31197
|
+
if (derived.size > 0) {
|
|
31198
|
+
let anchor = sourcesWithVar[0];
|
|
31199
|
+
for (const s of sourcesWithVar) {
|
|
31200
|
+
if (s.line < anchor.line) anchor = s;
|
|
31201
|
+
}
|
|
31202
|
+
const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
|
|
31203
|
+
for (const [varName] of derived) {
|
|
31204
|
+
if (!varName || existingVars.has(varName)) continue;
|
|
31205
|
+
sourcesWithVar.push({
|
|
31206
|
+
...anchor,
|
|
31207
|
+
variable: varName
|
|
31208
|
+
});
|
|
31209
|
+
existingVars.add(varName);
|
|
31210
|
+
}
|
|
31211
|
+
}
|
|
31212
|
+
}
|
|
31093
31213
|
const reCache = /* @__PURE__ */ new Map();
|
|
31094
31214
|
for (const s of sourcesWithVar) {
|
|
31095
31215
|
if (reCache.has(s.variable)) continue;
|
|
@@ -31266,7 +31386,15 @@ var TIER_1_METHOD_ANNOTATIONS = /* @__PURE__ */ new Set([
|
|
|
31266
31386
|
"DELETE",
|
|
31267
31387
|
"PATCH",
|
|
31268
31388
|
"HEAD",
|
|
31269
|
-
"OPTIONS"
|
|
31389
|
+
"OPTIONS",
|
|
31390
|
+
// Jenkins Stapler form-binding (#224 — CVE-2022-20617 docker-commons).
|
|
31391
|
+
// `@DataBoundConstructor` / `@DataBoundSetter` mark the trust boundary
|
|
31392
|
+
// between Jenkins UI (or `config.xml` unmarshal) and the plugin: the
|
|
31393
|
+
// Stapler runtime invokes the annotated constructor / setter with
|
|
31394
|
+
// user-supplied strings from a submitted form or a persisted job
|
|
31395
|
+
// config. Every parameter is a user-supplied taint source.
|
|
31396
|
+
"DataBoundConstructor",
|
|
31397
|
+
"DataBoundSetter"
|
|
31270
31398
|
]);
|
|
31271
31399
|
var TIER_1_CLASS_ANNOTATIONS = /* @__PURE__ */ new Set([
|
|
31272
31400
|
// Spring MVC
|
|
@@ -31309,7 +31437,30 @@ var TIER_1_BY_SUPERTYPE = /* @__PURE__ */ new Map([
|
|
|
31309
31437
|
["ChannelInboundHandler", /* @__PURE__ */ new Set(["channelRead", "channelReadComplete"])],
|
|
31310
31438
|
["ChannelInboundHandlerAdapter", /* @__PURE__ */ new Set(["channelRead", "channelReadComplete"])],
|
|
31311
31439
|
["ChannelDuplexHandler", /* @__PURE__ */ new Set(["channelRead", "channelReadComplete"])],
|
|
31312
|
-
["NettyRequestProcessor", /* @__PURE__ */ new Set(["process"])]
|
|
31440
|
+
["NettyRequestProcessor", /* @__PURE__ */ new Set(["process"])],
|
|
31441
|
+
// XStream deserialization converters (#224 — CVE-2020-26217,
|
|
31442
|
+
// CVE-2021-21345). The xstream deserializer invokes `unmarshal`
|
|
31443
|
+
// with attacker-controlled `HierarchicalStreamReader` state whenever
|
|
31444
|
+
// untrusted XML reaches `XStream.fromXML`. Each converter is a
|
|
31445
|
+
// deserialization gadget surface — its `unmarshal` parameters are
|
|
31446
|
+
// network-facing taint sources even though the enclosing class
|
|
31447
|
+
// carries no framework annotation. `marshal` is included for the
|
|
31448
|
+
// symmetric round-trip surface but is rarely exploitable on its
|
|
31449
|
+
// own; the fixup targets `unmarshal` primarily.
|
|
31450
|
+
//
|
|
31451
|
+
// `SingleValueConverter` is the string-form variant (fromString
|
|
31452
|
+
// parses an attacker string; toString is the sink half of the
|
|
31453
|
+
// round-trip). `ConverterMatcher` is the shared base and some
|
|
31454
|
+
// downstream projects subclass it directly.
|
|
31455
|
+
["Converter", /* @__PURE__ */ new Set(["marshal", "unmarshal"])],
|
|
31456
|
+
["SingleValueConverter", /* @__PURE__ */ new Set(["fromString", "toString"])],
|
|
31457
|
+
["ConverterMatcher", /* @__PURE__ */ new Set(["marshal", "unmarshal"])],
|
|
31458
|
+
// XStream abstract base classes — direct-parent `extends` match
|
|
31459
|
+
// covers the common shape where user converters subclass a base
|
|
31460
|
+
// rather than implementing `Converter` directly.
|
|
31461
|
+
["AbstractReflectionConverter", /* @__PURE__ */ new Set(["marshal", "unmarshal", "doMarshal", "doUnmarshal"])],
|
|
31462
|
+
["AbstractSingleValueConverter", /* @__PURE__ */ new Set(["fromString", "toString"])],
|
|
31463
|
+
["AbstractCollectionConverter", /* @__PURE__ */ new Set(["marshal", "unmarshal"])]
|
|
31313
31464
|
]);
|
|
31314
31465
|
var TIER_3_CLASS_SUFFIXES = [
|
|
31315
31466
|
"Util",
|
|
@@ -12577,7 +12577,20 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
12577
12577
|
line: paramLine,
|
|
12578
12578
|
confidence: param.type ? 0.7 : 0.5,
|
|
12579
12579
|
// Lower confidence for untyped params
|
|
12580
|
-
in_method: method.name
|
|
12580
|
+
in_method: method.name,
|
|
12581
|
+
// cognium-dev #220 — expose the parameter name to variable-scan flow
|
|
12582
|
+
// detection so uses of the param (including through concat-derived
|
|
12583
|
+
// aliases via `buildJavaTaintedVars`) can be bridged to sinks.
|
|
12584
|
+
//
|
|
12585
|
+
// Java-only: the Python/Rust alias-expansion branches in
|
|
12586
|
+
// taint-propagation-pass.ts use the earliest sourcesWithVar entry
|
|
12587
|
+
// as the anchor for synthetic derived sources. Adding
|
|
12588
|
+
// interprocedural_param to sourcesWithVar in those languages
|
|
12589
|
+
// would cause the anchor to become the low-confidence
|
|
12590
|
+
// interprocedural_param at the method decl line instead of the
|
|
12591
|
+
// real HTTP source, breaking downstream sink-type filters
|
|
12592
|
+
// (regressed #78, #92.1, #105 FP-31, #215 recall).
|
|
12593
|
+
...language === "java" ? { variable: param.name } : {}
|
|
12581
12594
|
});
|
|
12582
12595
|
}
|
|
12583
12596
|
}
|
|
@@ -13041,7 +13054,16 @@ function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
|
|
|
13041
13054
|
return false;
|
|
13042
13055
|
}
|
|
13043
13056
|
var TEMPLATE_LITERAL_RECEIVER_RE = /^Template\(\s*(?:"[^"\\]*"|'[^'\\]*')\s*\)$/;
|
|
13044
|
-
|
|
13057
|
+
var JINJA_AUTOESCAPE_TRUE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*True\b/;
|
|
13058
|
+
var JINJA_SELECT_AUTOESCAPE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*select_autoescape\s*\(/;
|
|
13059
|
+
var JINJA_AUTOESCAPE_FALSE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False\b/;
|
|
13060
|
+
function fileHasSafeJinjaEnvironment(sourceLines) {
|
|
13061
|
+
if (!sourceLines || sourceLines.length === 0) return false;
|
|
13062
|
+
const text = sourceLines.join("\n");
|
|
13063
|
+
if (JINJA_AUTOESCAPE_FALSE_RE.test(text)) return false;
|
|
13064
|
+
return JINJA_AUTOESCAPE_TRUE_RE.test(text) || JINJA_SELECT_AUTOESCAPE_RE.test(text);
|
|
13065
|
+
}
|
|
13066
|
+
function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
13045
13067
|
if (language !== "python") return false;
|
|
13046
13068
|
if (pattern.type !== "xss" && pattern.type !== "code_injection") return false;
|
|
13047
13069
|
const method = call.method_name;
|
|
@@ -13055,7 +13077,8 @@ function isSafeJinjaRenderCall(call, pattern, language) {
|
|
|
13055
13077
|
}
|
|
13056
13078
|
if (method === "render") {
|
|
13057
13079
|
const receiver = (call.receiver ?? "").trim();
|
|
13058
|
-
|
|
13080
|
+
if (TEMPLATE_LITERAL_RECEIVER_RE.test(receiver)) return true;
|
|
13081
|
+
if (fileHasSafeJinjaEnvironment(sourceLines)) return true;
|
|
13059
13082
|
}
|
|
13060
13083
|
return false;
|
|
13061
13084
|
}
|
|
@@ -13103,7 +13126,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13103
13126
|
if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
|
|
13104
13127
|
continue;
|
|
13105
13128
|
}
|
|
13106
|
-
if (isSafeJinjaRenderCall(call, pattern, language)) {
|
|
13129
|
+
if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
|
|
13107
13130
|
continue;
|
|
13108
13131
|
}
|
|
13109
13132
|
const location = formatCallLocation(call);
|
|
@@ -12511,7 +12511,20 @@ function findSources(calls, types, patterns, sourceLines, language) {
|
|
|
12511
12511
|
line: paramLine,
|
|
12512
12512
|
confidence: param.type ? 0.7 : 0.5,
|
|
12513
12513
|
// Lower confidence for untyped params
|
|
12514
|
-
in_method: method.name
|
|
12514
|
+
in_method: method.name,
|
|
12515
|
+
// cognium-dev #220 — expose the parameter name to variable-scan flow
|
|
12516
|
+
// detection so uses of the param (including through concat-derived
|
|
12517
|
+
// aliases via `buildJavaTaintedVars`) can be bridged to sinks.
|
|
12518
|
+
//
|
|
12519
|
+
// Java-only: the Python/Rust alias-expansion branches in
|
|
12520
|
+
// taint-propagation-pass.ts use the earliest sourcesWithVar entry
|
|
12521
|
+
// as the anchor for synthetic derived sources. Adding
|
|
12522
|
+
// interprocedural_param to sourcesWithVar in those languages
|
|
12523
|
+
// would cause the anchor to become the low-confidence
|
|
12524
|
+
// interprocedural_param at the method decl line instead of the
|
|
12525
|
+
// real HTTP source, breaking downstream sink-type filters
|
|
12526
|
+
// (regressed #78, #92.1, #105 FP-31, #215 recall).
|
|
12527
|
+
...language === "java" ? { variable: param.name } : {}
|
|
12515
12528
|
});
|
|
12516
12529
|
}
|
|
12517
12530
|
}
|
|
@@ -12975,7 +12988,16 @@ function isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines) {
|
|
|
12975
12988
|
return false;
|
|
12976
12989
|
}
|
|
12977
12990
|
var TEMPLATE_LITERAL_RECEIVER_RE = /^Template\(\s*(?:"[^"\\]*"|'[^'\\]*')\s*\)$/;
|
|
12978
|
-
|
|
12991
|
+
var JINJA_AUTOESCAPE_TRUE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*True\b/;
|
|
12992
|
+
var JINJA_SELECT_AUTOESCAPE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*select_autoescape\s*\(/;
|
|
12993
|
+
var JINJA_AUTOESCAPE_FALSE_RE = /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False\b/;
|
|
12994
|
+
function fileHasSafeJinjaEnvironment(sourceLines) {
|
|
12995
|
+
if (!sourceLines || sourceLines.length === 0) return false;
|
|
12996
|
+
const text = sourceLines.join("\n");
|
|
12997
|
+
if (JINJA_AUTOESCAPE_FALSE_RE.test(text)) return false;
|
|
12998
|
+
return JINJA_AUTOESCAPE_TRUE_RE.test(text) || JINJA_SELECT_AUTOESCAPE_RE.test(text);
|
|
12999
|
+
}
|
|
13000
|
+
function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
|
|
12979
13001
|
if (language !== "python") return false;
|
|
12980
13002
|
if (pattern.type !== "xss" && pattern.type !== "code_injection") return false;
|
|
12981
13003
|
const method = call.method_name;
|
|
@@ -12989,7 +13011,8 @@ function isSafeJinjaRenderCall(call, pattern, language) {
|
|
|
12989
13011
|
}
|
|
12990
13012
|
if (method === "render") {
|
|
12991
13013
|
const receiver = (call.receiver ?? "").trim();
|
|
12992
|
-
|
|
13014
|
+
if (TEMPLATE_LITERAL_RECEIVER_RE.test(receiver)) return true;
|
|
13015
|
+
if (fileHasSafeJinjaEnvironment(sourceLines)) return true;
|
|
12993
13016
|
}
|
|
12994
13017
|
return false;
|
|
12995
13018
|
}
|
|
@@ -13037,7 +13060,7 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
|
|
|
13037
13060
|
if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
|
|
13038
13061
|
continue;
|
|
13039
13062
|
}
|
|
13040
|
-
if (isSafeJinjaRenderCall(call, pattern, language)) {
|
|
13063
|
+
if (isSafeJinjaRenderCall(call, pattern, language, sourceLines)) {
|
|
13041
13064
|
continue;
|
|
13042
13065
|
}
|
|
13043
13066
|
const location = formatCallLocation(call);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circle-ir",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.145.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",
|