circle-ir 4.9.10 → 4.9.12

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.
@@ -15056,14 +15056,30 @@ var PYTHON_TAINTED_PATTERNS = [
15056
15056
  // regex registry so it's recognized in subscript/var contexts.
15057
15057
  { pattern: /\binput\s*\(/, sourceType: "io_input" }
15058
15058
  ];
15059
+ function dropCSharpObjectCarriedWaypoints(sinks, calls) {
15060
+ const zeroArgCtors = /* @__PURE__ */ new Set();
15061
+ for (const call of calls) {
15062
+ if (call.is_constructor && call.arguments.length === 0) {
15063
+ zeroArgCtors.add(`${call.location.line}:${call.method_name}`);
15064
+ }
15065
+ }
15066
+ return sinks.filter((sink) => {
15067
+ if (sink.type !== "sql_injection") return true;
15068
+ if (sink.method && zeroArgCtors.has(`${sink.line}:${sink.method}`)) {
15069
+ return false;
15070
+ }
15071
+ return true;
15072
+ });
15073
+ }
15059
15074
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
15060
15075
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
15061
15076
  const sources = findSources(calls, types, config.sources, sourceLines, language);
15062
15077
  let sinkPatterns = expandPromisifyAliases(config.sinks, sourceLines, language);
15063
15078
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
15064
15079
  const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
15080
+ const gatedSinks = language === "csharp" ? dropCSharpObjectCarriedWaypoints(sinks, calls) : sinks;
15065
15081
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
15066
- return { sources, sinks, sanitizers };
15082
+ return { sources, sinks: gatedSinks, sanitizers };
15067
15083
  }
15068
15084
  function sinkPatternAppliesTo(pattern, language) {
15069
15085
  if (language === void 0) return true;
@@ -15334,7 +15350,7 @@ function findSources(calls, types, patterns, sourceLines, language) {
15334
15350
  }
15335
15351
  const sourceMap = /* @__PURE__ */ new Map();
15336
15352
  for (const source of sources) {
15337
- const key = `${source.line}:${source.type}`;
15353
+ const key = source.type === "interprocedural_param" && source.variable ? `${source.line}:${source.type}:${source.variable}` : `${source.line}:${source.type}`;
15338
15354
  const existing = sourceMap.get(key);
15339
15355
  if (!existing || source.confidence > existing.confidence) {
15340
15356
  sourceMap.set(key, source);
@@ -15834,7 +15850,7 @@ function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)
15834
15850
  const guards = guardMap.get(cand);
15835
15851
  if (!guards) continue;
15836
15852
  for (const g of guards) {
15837
- if (g.i >= sinkIdx || checked.has(g)) continue;
15853
+ if (g.i > sinkIdx || checked.has(g)) continue;
15838
15854
  checked.add(g);
15839
15855
  const block = csThenBlock(sourceLines, g.i, g.rest);
15840
15856
  if (g.op === "==") {
@@ -15952,6 +15968,15 @@ var CWE_78_RECEIVER_ALLOWLIST = /* @__PURE__ */ new Set([
15952
15968
  // hutool
15953
15969
  "RuntimeUtil"
15954
15970
  ]);
15971
+ function isRegexReceiver(receiver, sourceLines) {
15972
+ const r = (receiver ?? "").trim();
15973
+ if (r.length === 0) return false;
15974
+ if (r.startsWith("/") && /\/[a-z]*$/.test(r)) return true;
15975
+ if (/^(?:new\s+)?RegExp\s*\(/.test(r)) return true;
15976
+ if (!/^[A-Za-z_$][\w$]*$/.test(r) || !sourceLines) return false;
15977
+ const decl = new RegExp(`(?:const|let|var)\\s+${escapeRe(r)}\\s*=\\s*(?:/|(?:new\\s+)?RegExp\\s*\\()`);
15978
+ return sourceLines.some((l) => decl.test(l));
15979
+ }
15955
15980
  function isFunctionCallbackArgument(arg) {
15956
15981
  if (arg.literal !== null && arg.literal !== void 0) return false;
15957
15982
  const expr = (arg.expression ?? "").trim();
@@ -16134,6 +16159,12 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
16134
16159
  continue;
16135
16160
  }
16136
16161
  }
16162
+ if (pattern.type === "command_injection" && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
16163
+ continue;
16164
+ }
16165
+ if (pattern.type === "code_injection" && language === "python" && call.method_name === "compile" && call.receiver && call.receiver !== "builtins") {
16166
+ continue;
16167
+ }
16137
16168
  if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
16138
16169
  continue;
16139
16170
  }
@@ -18784,6 +18815,9 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
18784
18815
  for (const source of sources) {
18785
18816
  const defsOnLine = defsByLine.get(source.line) ?? [];
18786
18817
  for (const def of defsOnLine) {
18818
+ if (source.type === "interprocedural_param" && source.variable && def.kind === "param" && def.variable !== source.variable) {
18819
+ continue;
18820
+ }
18787
18821
  tainted.push({
18788
18822
  variable: def.variable,
18789
18823
  defId: def.id,
@@ -34110,6 +34144,21 @@ function hasJavaImportFromPackage(packagePrefix, sourceLines) {
34110
34144
  }
34111
34145
  return false;
34112
34146
  }
34147
+ function hasSameOriginLiteralPrefix(lineText) {
34148
+ const m = lineText.match(/\(\s*(['"`])([^'"`]*)\1\s*(?:\+|,|\))/);
34149
+ const prefix = m?.[2];
34150
+ if (prefix === void 0) {
34151
+ const t = lineText.match(/\(\s*`([^`$]*)\$\{/);
34152
+ if (!t) return false;
34153
+ return isSameOriginQueryPrefix(t[1]);
34154
+ }
34155
+ return isSameOriginQueryPrefix(prefix);
34156
+ }
34157
+ function isSameOriginQueryPrefix(prefix) {
34158
+ if (!prefix.startsWith("/")) return false;
34159
+ if (prefix.startsWith("//")) return false;
34160
+ return prefix.includes("?");
34161
+ }
34113
34162
  var SinkFilterPass = class {
34114
34163
  name = "sink-filter";
34115
34164
  category = "security";
@@ -34265,6 +34314,9 @@ var SinkFilterPass = class {
34265
34314
  if (setHeaderMatch) {
34266
34315
  return false;
34267
34316
  }
34317
+ if (sink.type === "open_redirect" && hasSameOriginLiteralPrefix(sinkLineText)) {
34318
+ return false;
34319
+ }
34268
34320
  if (sink.method === "cookie" && sink.type === "crlf") {
34269
34321
  return false;
34270
34322
  }
@@ -36332,7 +36384,7 @@ var TaintPropagationPass = class {
36332
36384
  for (const f of paramFlows) {
36333
36385
  pushIfNew(f);
36334
36386
  }
36335
- const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language) ?? [];
36387
+ const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language, types) ?? [];
36336
36388
  for (const f of exprScanFlows) {
36337
36389
  if (flowKeys.has(flowKey(f))) continue;
36338
36390
  const flowForCheck = {
@@ -36484,6 +36536,36 @@ var TaintPropagationPass = class {
36484
36536
  return bestByKey.get(key) === f;
36485
36537
  });
36486
36538
  }
36539
+ if (ctx.language === "csharp" && finalFlows.length > 1) {
36540
+ const CSHARP_ADO_EXECUTE = /^Execute(?:Reader|NonQuery|Scalar)(?:Async)?$/;
36541
+ const commandTextReceiverByLine = /* @__PURE__ */ new Map();
36542
+ const execLinesByReceiver = /* @__PURE__ */ new Map();
36543
+ for (const call of calls) {
36544
+ const receiver = (call.receiver ?? "").trim();
36545
+ if (!receiver) continue;
36546
+ if (call.method_name === "CommandText") {
36547
+ commandTextReceiverByLine.set(call.location.line, receiver);
36548
+ } else if (CSHARP_ADO_EXECUTE.test(call.method_name)) {
36549
+ const lines = execLinesByReceiver.get(receiver) ?? [];
36550
+ lines.push(call.location.line);
36551
+ execLinesByReceiver.set(receiver, lines);
36552
+ }
36553
+ }
36554
+ if (commandTextReceiverByLine.size > 0) {
36555
+ const reportedSqlSinkLines = new Set(
36556
+ finalFlows.filter((f) => f.sink_type === "sql_injection").map((f) => f.sink_line)
36557
+ );
36558
+ finalFlows = finalFlows.filter((f) => {
36559
+ if (f.sink_type !== "sql_injection") return true;
36560
+ const receiver = commandTextReceiverByLine.get(f.sink_line);
36561
+ if (receiver === void 0) return true;
36562
+ const execLines = execLinesByReceiver.get(receiver) ?? [];
36563
+ return !execLines.some(
36564
+ (line) => line > f.sink_line && reportedSqlSinkLines.has(line)
36565
+ );
36566
+ });
36567
+ }
36568
+ }
36487
36569
  return { flows: finalFlows };
36488
36570
  }
36489
36571
  };
@@ -36901,7 +36983,7 @@ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
36901
36983
  }
36902
36984
  return false;
36903
36985
  }
36904
- function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language) {
36986
+ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language, types) {
36905
36987
  const flows = [];
36906
36988
  const sourcesWithVar = sources.filter(
36907
36989
  (s) => typeof s.variable === "string" && s.variable.length > 0
@@ -37158,12 +37240,32 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
37158
37240
  for (const s of sourcesWithVar) {
37159
37241
  if (s.line < anchor.line) anchor = s;
37160
37242
  }
37243
+ const methodAt = (line) => {
37244
+ for (const t of types ?? []) {
37245
+ for (const m of t.methods) {
37246
+ if (line >= m.start_line && line <= m.end_line) return m.name;
37247
+ }
37248
+ }
37249
+ return null;
37250
+ };
37161
37251
  const existingVars = new Set(sourcesWithVar.map((s) => s.variable));
37162
- for (const [varName] of derived) {
37252
+ for (const [varName, derivedLine] of derived) {
37163
37253
  if (!varName || existingVars.has(varName)) continue;
37254
+ const owner = methodAt(derivedLine);
37255
+ let scopedAnchor = anchor;
37256
+ if (owner) {
37257
+ let best;
37258
+ for (const s of sourcesWithVar) {
37259
+ const sOwner = s.in_method ?? methodAt(s.line);
37260
+ if (sOwner !== owner) continue;
37261
+ if (!best || s.line < best.line) best = s;
37262
+ }
37263
+ if (best) scopedAnchor = best;
37264
+ }
37164
37265
  sourcesWithVar.push({
37165
- ...anchor,
37166
- variable: varName
37266
+ ...scopedAnchor,
37267
+ variable: varName,
37268
+ ...owner ? { in_method: owner } : {}
37167
37269
  });
37168
37270
  existingVars.add(varName);
37169
37271
  }
@@ -14356,14 +14356,30 @@ var PYTHON_TAINTED_PATTERNS = [
14356
14356
  // regex registry so it's recognized in subscript/var contexts.
14357
14357
  { pattern: /\binput\s*\(/, sourceType: "io_input" }
14358
14358
  ];
14359
+ function dropCSharpObjectCarriedWaypoints(sinks, calls) {
14360
+ const zeroArgCtors = /* @__PURE__ */ new Set();
14361
+ for (const call of calls) {
14362
+ if (call.is_constructor && call.arguments.length === 0) {
14363
+ zeroArgCtors.add(`${call.location.line}:${call.method_name}`);
14364
+ }
14365
+ }
14366
+ return sinks.filter((sink) => {
14367
+ if (sink.type !== "sql_injection") return true;
14368
+ if (sink.method && zeroArgCtors.has(`${sink.line}:${sink.method}`)) {
14369
+ return false;
14370
+ }
14371
+ return true;
14372
+ });
14373
+ }
14359
14374
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
14360
14375
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
14361
14376
  const sources = findSources(calls, types, config.sources, sourceLines, language);
14362
14377
  let sinkPatterns = expandPromisifyAliases(config.sinks, sourceLines, language);
14363
14378
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
14364
14379
  const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
14380
+ const gatedSinks = language === "csharp" ? dropCSharpObjectCarriedWaypoints(sinks, calls) : sinks;
14365
14381
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
14366
- return { sources, sinks, sanitizers };
14382
+ return { sources, sinks: gatedSinks, sanitizers };
14367
14383
  }
14368
14384
  function sinkPatternAppliesTo(pattern, language) {
14369
14385
  if (language === void 0) return true;
@@ -14634,7 +14650,7 @@ function findSources(calls, types, patterns, sourceLines, language) {
14634
14650
  }
14635
14651
  const sourceMap = /* @__PURE__ */ new Map();
14636
14652
  for (const source of sources) {
14637
- const key = `${source.line}:${source.type}`;
14653
+ const key = source.type === "interprocedural_param" && source.variable ? `${source.line}:${source.type}:${source.variable}` : `${source.line}:${source.type}`;
14638
14654
  const existing = sourceMap.get(key);
14639
14655
  if (!existing || source.confidence > existing.confidence) {
14640
14656
  sourceMap.set(key, source);
@@ -15134,7 +15150,7 @@ function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)
15134
15150
  const guards = guardMap.get(cand);
15135
15151
  if (!guards) continue;
15136
15152
  for (const g of guards) {
15137
- if (g.i >= sinkIdx || checked.has(g)) continue;
15153
+ if (g.i > sinkIdx || checked.has(g)) continue;
15138
15154
  checked.add(g);
15139
15155
  const block = csThenBlock(sourceLines, g.i, g.rest);
15140
15156
  if (g.op === "==") {
@@ -15252,6 +15268,15 @@ var CWE_78_RECEIVER_ALLOWLIST = /* @__PURE__ */ new Set([
15252
15268
  // hutool
15253
15269
  "RuntimeUtil"
15254
15270
  ]);
15271
+ function isRegexReceiver(receiver, sourceLines) {
15272
+ const r = (receiver ?? "").trim();
15273
+ if (r.length === 0) return false;
15274
+ if (r.startsWith("/") && /\/[a-z]*$/.test(r)) return true;
15275
+ if (/^(?:new\s+)?RegExp\s*\(/.test(r)) return true;
15276
+ if (!/^[A-Za-z_$][\w$]*$/.test(r) || !sourceLines) return false;
15277
+ const decl = new RegExp(`(?:const|let|var)\\s+${escapeRe(r)}\\s*=\\s*(?:/|(?:new\\s+)?RegExp\\s*\\()`);
15278
+ return sourceLines.some((l) => decl.test(l));
15279
+ }
15255
15280
  function isFunctionCallbackArgument(arg) {
15256
15281
  if (arg.literal !== null && arg.literal !== void 0) return false;
15257
15282
  const expr = (arg.expression ?? "").trim();
@@ -15434,6 +15459,12 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
15434
15459
  continue;
15435
15460
  }
15436
15461
  }
15462
+ if (pattern.type === "command_injection" && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
15463
+ continue;
15464
+ }
15465
+ if (pattern.type === "code_injection" && language === "python" && call.method_name === "compile" && call.receiver && call.receiver !== "builtins") {
15466
+ continue;
15467
+ }
15437
15468
  if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
15438
15469
  continue;
15439
15470
  }
@@ -16749,6 +16780,9 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
16749
16780
  for (const source of sources) {
16750
16781
  const defsOnLine = defsByLine.get(source.line) ?? [];
16751
16782
  for (const def of defsOnLine) {
16783
+ if (source.type === "interprocedural_param" && source.variable && def.kind === "param" && def.variable !== source.variable) {
16784
+ continue;
16785
+ }
16752
16786
  tainted.push({
16753
16787
  variable: def.variable,
16754
16788
  defId: def.id,
@@ -14290,14 +14290,30 @@ var PYTHON_TAINTED_PATTERNS = [
14290
14290
  // regex registry so it's recognized in subscript/var contexts.
14291
14291
  { pattern: /\binput\s*\(/, sourceType: "io_input" }
14292
14292
  ];
14293
+ function dropCSharpObjectCarriedWaypoints(sinks, calls) {
14294
+ const zeroArgCtors = /* @__PURE__ */ new Set();
14295
+ for (const call of calls) {
14296
+ if (call.is_constructor && call.arguments.length === 0) {
14297
+ zeroArgCtors.add(`${call.location.line}:${call.method_name}`);
14298
+ }
14299
+ }
14300
+ return sinks.filter((sink) => {
14301
+ if (sink.type !== "sql_injection") return true;
14302
+ if (sink.method && zeroArgCtors.has(`${sink.line}:${sink.method}`)) {
14303
+ return false;
14304
+ }
14305
+ return true;
14306
+ });
14307
+ }
14293
14308
  function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy, language, code) {
14294
14309
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
14295
14310
  const sources = findSources(calls, types, config.sources, sourceLines, language);
14296
14311
  let sinkPatterns = expandPromisifyAliases(config.sinks, sourceLines, language);
14297
14312
  sinkPatterns = expandIndirectEvalAliases(sinkPatterns, sourceLines, language);
14298
14313
  const sinks = findSinks(calls, sinkPatterns, typeHierarchy, language, sourceLines, types);
14314
+ const gatedSinks = language === "csharp" ? dropCSharpObjectCarriedWaypoints(sinks, calls) : sinks;
14299
14315
  const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
14300
- return { sources, sinks, sanitizers };
14316
+ return { sources, sinks: gatedSinks, sanitizers };
14301
14317
  }
14302
14318
  function sinkPatternAppliesTo(pattern, language) {
14303
14319
  if (language === void 0) return true;
@@ -14568,7 +14584,7 @@ function findSources(calls, types, patterns, sourceLines, language) {
14568
14584
  }
14569
14585
  const sourceMap = /* @__PURE__ */ new Map();
14570
14586
  for (const source of sources) {
14571
- const key = `${source.line}:${source.type}`;
14587
+ const key = source.type === "interprocedural_param" && source.variable ? `${source.line}:${source.type}:${source.variable}` : `${source.line}:${source.type}`;
14572
14588
  const existing = sourceMap.get(key);
14573
14589
  if (!existing || source.confidence > existing.confidence) {
14574
14590
  sourceMap.set(key, source);
@@ -15068,7 +15084,7 @@ function isCSharpSsrfHostAllowlistGuarded(call, pattern, language, sourceLines)
15068
15084
  const guards = guardMap.get(cand);
15069
15085
  if (!guards) continue;
15070
15086
  for (const g of guards) {
15071
- if (g.i >= sinkIdx || checked.has(g)) continue;
15087
+ if (g.i > sinkIdx || checked.has(g)) continue;
15072
15088
  checked.add(g);
15073
15089
  const block = csThenBlock(sourceLines, g.i, g.rest);
15074
15090
  if (g.op === "==") {
@@ -15186,6 +15202,15 @@ var CWE_78_RECEIVER_ALLOWLIST = /* @__PURE__ */ new Set([
15186
15202
  // hutool
15187
15203
  "RuntimeUtil"
15188
15204
  ]);
15205
+ function isRegexReceiver(receiver, sourceLines) {
15206
+ const r = (receiver ?? "").trim();
15207
+ if (r.length === 0) return false;
15208
+ if (r.startsWith("/") && /\/[a-z]*$/.test(r)) return true;
15209
+ if (/^(?:new\s+)?RegExp\s*\(/.test(r)) return true;
15210
+ if (!/^[A-Za-z_$][\w$]*$/.test(r) || !sourceLines) return false;
15211
+ const decl = new RegExp(`(?:const|let|var)\\s+${escapeRe(r)}\\s*=\\s*(?:/|(?:new\\s+)?RegExp\\s*\\()`);
15212
+ return sourceLines.some((l) => decl.test(l));
15213
+ }
15189
15214
  function isFunctionCallbackArgument(arg) {
15190
15215
  if (arg.literal !== null && arg.literal !== void 0) return false;
15191
15216
  const expr = (arg.expression ?? "").trim();
@@ -15368,6 +15393,12 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
15368
15393
  continue;
15369
15394
  }
15370
15395
  }
15396
+ if (pattern.type === "command_injection" && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
15397
+ continue;
15398
+ }
15399
+ if (pattern.type === "code_injection" && language === "python" && call.method_name === "compile" && call.receiver && call.receiver !== "builtins") {
15400
+ continue;
15401
+ }
15371
15402
  if (isSafeGoJsonUnmarshalCall(call, pattern, language, sourceLines)) {
15372
15403
  continue;
15373
15404
  }
@@ -16683,6 +16714,9 @@ function findInitialTaint(sources, callsByLine, defsByLine) {
16683
16714
  for (const source of sources) {
16684
16715
  const defsOnLine = defsByLine.get(source.line) ?? [];
16685
16716
  for (const def of defsOnLine) {
16717
+ if (source.type === "interprocedural_param" && source.variable && def.kind === "param" && def.variable !== source.variable) {
16718
+ continue;
16719
+ }
16686
16720
  tainted.push({
16687
16721
  variable: def.variable,
16688
16722
  defId: def.id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "4.9.10",
3
+ "version": "4.9.12",
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",