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.
@@ -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
- function isSafeJinjaRenderCall(call, pattern, language) {
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
- return TEMPLATE_LITERAL_RECEIVER_RE.test(receiver);
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
- function isSafeJinjaRenderCall(call, pattern, language) {
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
- return TEMPLATE_LITERAL_RECEIVER_RE.test(receiver);
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
- function isSafeJinjaRenderCall(call, pattern, language) {
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
- return TEMPLATE_LITERAL_RECEIVER_RE.test(receiver);
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.144.0",
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",