circle-ir 3.73.0 → 3.75.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.
@@ -11795,6 +11795,18 @@ var DEFAULT_SINKS = [
11795
11795
  // value position so a tainted variable is detected.
11796
11796
  { method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11797
11797
  { method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11798
+ // Python: Flask/Werkzeug/FastAPI/Django response header sinks (CWE-113).
11799
+ // Subscript assignment (`resp.headers['X-A'] = name`) is NOT covered because
11800
+ // the IR does not emit subscript writes as calls — a known limitation, see
11801
+ // cognium-dev #111. The method-call forms below ARE captured (receiver
11802
+ // suffix-match on `.headers` via receiverMightBeClass).
11803
+ { method: "set", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11804
+ { method: "add", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11805
+ { method: "setdefault", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11806
+ { method: "extend", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["python"] },
11807
+ { method: "__setitem__", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11808
+ // Flask/Werkzeug response.set_cookie(name, value, ...) — value is CRLF-sensitive.
11809
+ { method: "set_cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11798
11810
  // Mass-assignment (CWE-915 / CWE-1321) — Sprint 6, #86; cognium-dev #68 Sprint 10.
11799
11811
  // JS Object.assign(target, ...sources), `_.merge`, `_.extend`, `$.extend`,
11800
11812
  // `Object.defineProperty` — when fed an attacker-controlled bag, they write
@@ -11884,11 +11896,21 @@ var DEFAULT_SANITIZERS = [
11884
11896
  // (defense-in-depth — mirrors Java getCanonicalPath in this table; the
11885
11897
  // stricter Clean+HasPrefix guard recognition is tracked separately).
11886
11898
  // EvalSymlinks is the Go equivalent of Java's Path.toRealPath.
11887
- { method: "Base", class: "filepath", removes: ["path_traversal"] },
11888
- { method: "Base", class: "path", removes: ["path_traversal"] },
11889
- { method: "Clean", class: "filepath", removes: ["path_traversal"] },
11890
- { method: "Clean", class: "path", removes: ["path_traversal"] },
11891
- { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal"] },
11899
+ // Sprint 24 (#102 FP-27): broadened to cover external_taint_escape (CWE-668)
11900
+ // fallback so canonicalised paths don't trigger the synthetic sink.
11901
+ { method: "Base", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
11902
+ { method: "Base", class: "path", removes: ["path_traversal", "external_taint_escape"] },
11903
+ { method: "Clean", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
11904
+ { method: "Clean", class: "path", removes: ["path_traversal", "external_taint_escape"] },
11905
+ { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
11906
+ // Go html/template escape helpers (#102 FP-27) — registered explicitly because
11907
+ // configs/sinks/golang.json is not loaded at runtime.
11908
+ { method: "EscapeString", class: "html", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
11909
+ { method: "HTMLEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
11910
+ { method: "JSEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection"] },
11911
+ { method: "URLQueryEscaper", class: "template", removes: ["xss", "external_taint_escape", "open_redirect"] },
11912
+ { method: "QueryEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
11913
+ { method: "PathEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
11892
11914
  // Log Injection sanitizers
11893
11915
  { method: "replace", removes: ["log_injection"] },
11894
11916
  // Used to remove newlines/control chars
@@ -12408,6 +12430,42 @@ function isSafePythonSubprocessCall(call, pattern, language) {
12408
12430
  }
12409
12431
  return true;
12410
12432
  }
12433
+ function isSafeGoExecCommandCall(call, pattern, language) {
12434
+ if (language !== "go") return false;
12435
+ if (pattern.type !== "command_injection") return false;
12436
+ if (pattern.class !== "exec") return false;
12437
+ if (pattern.method !== "Command" && pattern.method !== "CommandContext") return false;
12438
+ const programArgPos = pattern.method === "CommandContext" ? 1 : 0;
12439
+ const programArg = call.arguments.find((a) => a.position === programArgPos);
12440
+ if (!programArg) return false;
12441
+ let program;
12442
+ if (programArg.literal !== null && programArg.literal !== void 0) {
12443
+ program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
12444
+ } else {
12445
+ const expr = (programArg.expression ?? "").trim();
12446
+ if (!(expr.startsWith('"') || expr.startsWith("`") || expr.startsWith("'"))) {
12447
+ return false;
12448
+ }
12449
+ const stripped = expr.slice(1, -1);
12450
+ program = stripped.split("/").pop() ?? stripped;
12451
+ }
12452
+ const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
12453
+ "sh",
12454
+ "bash",
12455
+ "zsh",
12456
+ "dash",
12457
+ "ash",
12458
+ "ksh",
12459
+ "cmd",
12460
+ "cmd.exe",
12461
+ "powershell",
12462
+ "pwsh",
12463
+ "powershell.exe",
12464
+ "pwsh.exe"
12465
+ ]);
12466
+ if (SHELL_PROGRAMS.has(program)) return false;
12467
+ return true;
12468
+ }
12411
12469
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
12412
12470
  function argIsClassLiteral(call, position) {
12413
12471
  const arg = call.arguments.find((a) => a.position === position);
@@ -12427,6 +12485,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12427
12485
  if (isSafePythonSubprocessCall(call, pattern, language)) {
12428
12486
  continue;
12429
12487
  }
12488
+ if (isSafeGoExecCommandCall(call, pattern, language)) {
12489
+ continue;
12490
+ }
12430
12491
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
12431
12492
  continue;
12432
12493
  }
@@ -12785,6 +12846,10 @@ function receiverMightBeClass(receiver, className) {
12785
12846
  }
12786
12847
  }
12787
12848
  }
12849
+ const chainedCallSuffix = `.${className}()`;
12850
+ if (receiver.endsWith(chainedCallSuffix) || receiver.toLowerCase().endsWith(chainedCallSuffix.toLowerCase())) {
12851
+ return true;
12852
+ }
12788
12853
  if (receiver.includes("::")) {
12789
12854
  const scopePrefix = receiver.match(/^(\w+)::/);
12790
12855
  if (scopePrefix) {
@@ -11729,6 +11729,18 @@ var DEFAULT_SINKS = [
11729
11729
  // value position so a tainted variable is detected.
11730
11730
  { method: "Set", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11731
11731
  { method: "Add", class: "Header", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["go"] },
11732
+ // Python: Flask/Werkzeug/FastAPI/Django response header sinks (CWE-113).
11733
+ // Subscript assignment (`resp.headers['X-A'] = name`) is NOT covered because
11734
+ // the IR does not emit subscript writes as calls — a known limitation, see
11735
+ // cognium-dev #111. The method-call forms below ARE captured (receiver
11736
+ // suffix-match on `.headers` via receiverMightBeClass).
11737
+ { method: "set", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11738
+ { method: "add", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11739
+ { method: "setdefault", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11740
+ { method: "extend", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [0], languages: ["python"] },
11741
+ { method: "__setitem__", class: "headers", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11742
+ // Flask/Werkzeug response.set_cookie(name, value, ...) — value is CRLF-sensitive.
11743
+ { method: "set_cookie", type: "crlf", cwe: "CWE-113", severity: "medium", arg_positions: [1], languages: ["python"] },
11732
11744
  // Mass-assignment (CWE-915 / CWE-1321) — Sprint 6, #86; cognium-dev #68 Sprint 10.
11733
11745
  // JS Object.assign(target, ...sources), `_.merge`, `_.extend`, `$.extend`,
11734
11746
  // `Object.defineProperty` — when fed an attacker-controlled bag, they write
@@ -11818,11 +11830,21 @@ var DEFAULT_SANITIZERS = [
11818
11830
  // (defense-in-depth — mirrors Java getCanonicalPath in this table; the
11819
11831
  // stricter Clean+HasPrefix guard recognition is tracked separately).
11820
11832
  // EvalSymlinks is the Go equivalent of Java's Path.toRealPath.
11821
- { method: "Base", class: "filepath", removes: ["path_traversal"] },
11822
- { method: "Base", class: "path", removes: ["path_traversal"] },
11823
- { method: "Clean", class: "filepath", removes: ["path_traversal"] },
11824
- { method: "Clean", class: "path", removes: ["path_traversal"] },
11825
- { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal"] },
11833
+ // Sprint 24 (#102 FP-27): broadened to cover external_taint_escape (CWE-668)
11834
+ // fallback so canonicalised paths don't trigger the synthetic sink.
11835
+ { method: "Base", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
11836
+ { method: "Base", class: "path", removes: ["path_traversal", "external_taint_escape"] },
11837
+ { method: "Clean", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
11838
+ { method: "Clean", class: "path", removes: ["path_traversal", "external_taint_escape"] },
11839
+ { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal", "external_taint_escape"] },
11840
+ // Go html/template escape helpers (#102 FP-27) — registered explicitly because
11841
+ // configs/sinks/golang.json is not loaded at runtime.
11842
+ { method: "EscapeString", class: "html", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
11843
+ { method: "HTMLEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection", "open_redirect"] },
11844
+ { method: "JSEscapeString", class: "template", removes: ["xss", "external_taint_escape", "log_injection"] },
11845
+ { method: "URLQueryEscaper", class: "template", removes: ["xss", "external_taint_escape", "open_redirect"] },
11846
+ { method: "QueryEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
11847
+ { method: "PathEscape", class: "url", removes: ["xss", "external_taint_escape", "open_redirect"] },
11826
11848
  // Log Injection sanitizers
11827
11849
  { method: "replace", removes: ["log_injection"] },
11828
11850
  // Used to remove newlines/control chars
@@ -12342,6 +12364,42 @@ function isSafePythonSubprocessCall(call, pattern, language) {
12342
12364
  }
12343
12365
  return true;
12344
12366
  }
12367
+ function isSafeGoExecCommandCall(call, pattern, language) {
12368
+ if (language !== "go") return false;
12369
+ if (pattern.type !== "command_injection") return false;
12370
+ if (pattern.class !== "exec") return false;
12371
+ if (pattern.method !== "Command" && pattern.method !== "CommandContext") return false;
12372
+ const programArgPos = pattern.method === "CommandContext" ? 1 : 0;
12373
+ const programArg = call.arguments.find((a) => a.position === programArgPos);
12374
+ if (!programArg) return false;
12375
+ let program;
12376
+ if (programArg.literal !== null && programArg.literal !== void 0) {
12377
+ program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
12378
+ } else {
12379
+ const expr = (programArg.expression ?? "").trim();
12380
+ if (!(expr.startsWith('"') || expr.startsWith("`") || expr.startsWith("'"))) {
12381
+ return false;
12382
+ }
12383
+ const stripped = expr.slice(1, -1);
12384
+ program = stripped.split("/").pop() ?? stripped;
12385
+ }
12386
+ const SHELL_PROGRAMS = /* @__PURE__ */ new Set([
12387
+ "sh",
12388
+ "bash",
12389
+ "zsh",
12390
+ "dash",
12391
+ "ash",
12392
+ "ksh",
12393
+ "cmd",
12394
+ "cmd.exe",
12395
+ "powershell",
12396
+ "pwsh",
12397
+ "powershell.exe",
12398
+ "pwsh.exe"
12399
+ ]);
12400
+ if (SHELL_PROGRAMS.has(program)) return false;
12401
+ return true;
12402
+ }
12345
12403
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
12346
12404
  function argIsClassLiteral(call, position) {
12347
12405
  const arg = call.arguments.find((a) => a.position === position);
@@ -12361,6 +12419,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12361
12419
  if (isSafePythonSubprocessCall(call, pattern, language)) {
12362
12420
  continue;
12363
12421
  }
12422
+ if (isSafeGoExecCommandCall(call, pattern, language)) {
12423
+ continue;
12424
+ }
12364
12425
  if (pattern.safe_if_class_literal_at !== void 0 && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
12365
12426
  continue;
12366
12427
  }
@@ -12719,6 +12780,10 @@ function receiverMightBeClass(receiver, className) {
12719
12780
  }
12720
12781
  }
12721
12782
  }
12783
+ const chainedCallSuffix = `.${className}()`;
12784
+ if (receiver.endsWith(chainedCallSuffix) || receiver.toLowerCase().endsWith(chainedCallSuffix.toLowerCase())) {
12785
+ return true;
12786
+ }
12722
12787
  if (receiver.includes("::")) {
12723
12788
  const scopePrefix = receiver.match(/^(\w+)::/);
12724
12789
  if (scopePrefix) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.73.0",
3
+ "version": "3.75.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",