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.
- package/configs/sinks/golang.json +8 -8
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +27 -5
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/interprocedural.d.ts.map +1 -1
- package/dist/analysis/interprocedural.js +16 -0
- package/dist/analysis/interprocedural.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +58 -1
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +116 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +54 -0
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/passes/weak-random-pass.d.ts.map +1 -1
- package/dist/analysis/passes/weak-random-pass.js +11 -0
- package/dist/analysis/passes/weak-random-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +83 -0
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +242 -7
- package/dist/core/circle-ir-core.cjs +70 -5
- package/dist/core/circle-ir-core.js +70 -5
- package/package.json +1 -1
|
@@ -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
|
-
|
|
11888
|
-
|
|
11889
|
-
{ method: "
|
|
11890
|
-
{ method: "
|
|
11891
|
-
{ method: "
|
|
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
|
-
|
|
11822
|
-
|
|
11823
|
-
{ method: "
|
|
11824
|
-
{ method: "
|
|
11825
|
-
{ method: "
|
|
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.
|
|
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",
|