circle-ir 3.182.0 → 3.185.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/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +97 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +207 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.js +27 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/browser/circle-ir.js +227 -2
- package/dist/core/circle-ir-core.cjs +105 -1
- package/dist/core/circle-ir-core.js +105 -1
- package/package.json +1 -1
|
@@ -11326,6 +11326,49 @@ var DEFAULT_SOURCES = [
|
|
|
11326
11326
|
// Server-side interceptor receives `Metadata headers`; `headers.get(KEY)`
|
|
11327
11327
|
// returns caller-supplied header values.
|
|
11328
11328
|
{ method: "get", class: "Metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["java"] },
|
|
11329
|
+
// --- WebSocket transport channels (cognium-dev #213 second slice) ---
|
|
11330
|
+
//
|
|
11331
|
+
// Server-side WebSocket handlers receive attacker-authored frames on
|
|
11332
|
+
// every call to a receive-shaped method. Untrusted the moment they
|
|
11333
|
+
// return, regardless of any application-level auth on the socket.
|
|
11334
|
+
//
|
|
11335
|
+
// Python — FastAPI / Starlette (`from fastapi import WebSocket`):
|
|
11336
|
+
// data = await websocket.receive_text()
|
|
11337
|
+
// data = await websocket.receive_bytes()
|
|
11338
|
+
// data = await websocket.receive_json() # parsed dict/list
|
|
11339
|
+
// data = await websocket.receive() # {'type', 'text'|'bytes'}
|
|
11340
|
+
{ method: "receive_text", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11341
|
+
{ method: "receive_bytes", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11342
|
+
{ method: "receive_json", class: "WebSocket", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11343
|
+
//
|
|
11344
|
+
// `receive` intentionally class-scoped to WebSocket (Starlette pattern).
|
|
11345
|
+
// Broadening to unqualified would collide with queue/signal `.receive()`.
|
|
11346
|
+
{ method: "receive", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11347
|
+
// Django Channels `AsyncJsonWebsocketConsumer` / `WebsocketConsumer`
|
|
11348
|
+
// expose the same receive_* names on `self`; the class filter matches
|
|
11349
|
+
// `WebSocket` only, so add unqualified fallbacks for the receive_json /
|
|
11350
|
+
// receive_text convention (broad — no class filter possible without
|
|
11351
|
+
// hardcoding Django's consumer names).
|
|
11352
|
+
{ method: "receive_json", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11353
|
+
{ method: "receive_text", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11354
|
+
{ method: "receive_bytes", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
11355
|
+
// Go — gorilla/websocket (`github.com/gorilla/websocket`):
|
|
11356
|
+
// messageType, message, err := conn.ReadMessage()
|
|
11357
|
+
// _, r, err := conn.NextReader()
|
|
11358
|
+
{ method: "ReadMessage", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
11359
|
+
{ method: "NextReader", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
11360
|
+
// nhooyr.io/websocket exposes a `Read` method on `*websocket.Conn`;
|
|
11361
|
+
// signature is `func (c *Conn) Read(ctx) (MessageType, []byte, error)`.
|
|
11362
|
+
{ method: "Read", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
11363
|
+
// Java — Jakarta / Java WebSocket API (`javax.websocket` / `jakarta.websocket`):
|
|
11364
|
+
// session.getBasicRemote().sendText(input); // OUT (not a source)
|
|
11365
|
+
// @OnMessage public void onMessage(String msg) — `msg` is a callback
|
|
11366
|
+
// param, not a return; handled via param_tainted when annotation is set.
|
|
11367
|
+
{ annotation: "OnMessage", type: "network_input", severity: "high", param_tainted: true, languages: ["java"] },
|
|
11368
|
+
// Spring `@MessageMapping` STOMP-over-WebSocket handlers receive
|
|
11369
|
+
// untrusted payloads on every dispatched frame.
|
|
11370
|
+
{ annotation: "MessageMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
|
|
11371
|
+
{ annotation: "SubscribeMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
|
|
11329
11372
|
// --- Cache reads (second-order taint — Redis / Memcached / Django cache) ---
|
|
11330
11373
|
// The cache round-trip is a canonical second-order sink: whatever was
|
|
11331
11374
|
// written previously (potentially attacker-controlled) resurfaces on read.
|
|
@@ -13398,6 +13441,60 @@ var DEFAULT_SANITIZERS = [
|
|
|
13398
13441
|
// Python Type coercion
|
|
13399
13442
|
{ method: "int", removes: ["sql_injection", "command_injection", "xss"] },
|
|
13400
13443
|
{ method: "float", removes: ["sql_injection", "command_injection"] },
|
|
13444
|
+
// Python URL encoding (cognium-dev #213 fifth slice).
|
|
13445
|
+
//
|
|
13446
|
+
// urllib.parse.quote(x) — RFC-3986 percent-encode; safe for URL path
|
|
13447
|
+
// urllib.parse.quote_plus(x) — same + space→+; safe for query string
|
|
13448
|
+
// urllib.parse.urlencode(d) — encode a dict of pairs
|
|
13449
|
+
//
|
|
13450
|
+
// Bounded to URL-context sinks. Class-scoped to the specific
|
|
13451
|
+
// `urllib.parse` receiver to avoid colliding with unrelated bare
|
|
13452
|
+
// `quote(...)` calls in other libraries.
|
|
13453
|
+
{ method: "quote", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
13454
|
+
{ method: "quote_plus", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
13455
|
+
{ method: "urlencode", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss"] },
|
|
13456
|
+
// Bare aliases — `from urllib.parse import quote` then unqualified.
|
|
13457
|
+
{ method: "quote_plus", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
13458
|
+
{ method: "urlencode", removes: ["ssrf", "open_redirect", "xss"] },
|
|
13459
|
+
// `quote` bare is intentionally NOT registered — it collides with
|
|
13460
|
+
// shlex.quote (bare-imported) which is a command_injection sanitizer,
|
|
13461
|
+
// not a URL sanitizer. The class-scoped variant above catches the
|
|
13462
|
+
// qualified `urllib.parse.quote(...)` shape; unqualified callers who
|
|
13463
|
+
// want URL-context credit should use `quote_plus` (which does not
|
|
13464
|
+
// collide with any command_injection sanitizer).
|
|
13465
|
+
// Python XSS — additional common sanitizers.
|
|
13466
|
+
//
|
|
13467
|
+
// bleach.clean(...) — already covered above (line 2898)
|
|
13468
|
+
// bleach.linkify(...) — turns URLs into anchor tags; sanitizes as well
|
|
13469
|
+
// django.utils.html.escape / strip_tags — Django's XSS escape helpers
|
|
13470
|
+
// jinja2.escape — Jinja2's Markup escape (aliased from markupsafe)
|
|
13471
|
+
// flask.escape — Flask re-export of markupsafe.escape
|
|
13472
|
+
// saxutils.escape — stdlib xml.sax.saxutils.escape for XML docs
|
|
13473
|
+
{ method: "linkify", class: "bleach", removes: ["xss"] },
|
|
13474
|
+
// Bare `linkify(...)` alias — `from bleach import linkify`.
|
|
13475
|
+
{ method: "linkify", removes: ["xss"] },
|
|
13476
|
+
{ method: "escape", class: "django.utils.html", removes: ["xss"] },
|
|
13477
|
+
{ method: "strip_tags", class: "django.utils.html", removes: ["xss"] },
|
|
13478
|
+
{ method: "escape", class: "jinja2", removes: ["xss"] },
|
|
13479
|
+
{ method: "escape", class: "flask", removes: ["xss"] },
|
|
13480
|
+
{ method: "escape", class: "saxutils", removes: ["xss"] },
|
|
13481
|
+
{ method: "escape", class: "xml.sax.saxutils", removes: ["xss"] },
|
|
13482
|
+
{ method: "quoteattr", class: "saxutils", removes: ["xss"] },
|
|
13483
|
+
{ method: "quoteattr", class: "xml.sax.saxutils", removes: ["xss"] },
|
|
13484
|
+
// Python ReDoS — `re.escape(user)` when building a regex from user input
|
|
13485
|
+
// strips regex metacharacters. Downstream `re.compile / re.match` cannot
|
|
13486
|
+
// interpret user-supplied alternations or quantifiers. Also covers the
|
|
13487
|
+
// `code_injection` categorization that `re.compile` currently emits
|
|
13488
|
+
// (a re.escape-wrapped pattern cannot execute anything, so both are safe).
|
|
13489
|
+
{ method: "escape", class: "re", removes: ["redos", "code_injection"] },
|
|
13490
|
+
// Python SQLAlchemy — `text(...).bindparams(...)` binds params safely.
|
|
13491
|
+
// The `bindparams` call is the sanitizer; the parent `text` wraps the
|
|
13492
|
+
// template. Also add `expression.literal` for explicit SQL literals.
|
|
13493
|
+
{ method: "bindparams", removes: ["sql_injection"] },
|
|
13494
|
+
// psycopg2 sql-composition helpers
|
|
13495
|
+
{ method: "Identifier", class: "sql", removes: ["sql_injection"] },
|
|
13496
|
+
{ method: "Literal", class: "sql", removes: ["sql_injection"] },
|
|
13497
|
+
{ method: "Placeholder", class: "sql", removes: ["sql_injection"] },
|
|
13401
13498
|
// =========================================================================
|
|
13402
13499
|
// Rust Sanitizers
|
|
13403
13500
|
// =========================================================================
|
|
@@ -15321,7 +15418,14 @@ function matchesSanitizerPattern(call, pattern) {
|
|
|
15321
15418
|
return false;
|
|
15322
15419
|
}
|
|
15323
15420
|
if (pattern.class) {
|
|
15324
|
-
if (!call.receiver
|
|
15421
|
+
if (!call.receiver) {
|
|
15422
|
+
const target = call.resolution?.target;
|
|
15423
|
+
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
15424
|
+
if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
|
|
15425
|
+
} else {
|
|
15426
|
+
return false;
|
|
15427
|
+
}
|
|
15428
|
+
} else if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
15325
15429
|
return false;
|
|
15326
15430
|
}
|
|
15327
15431
|
}
|
|
@@ -25647,7 +25751,22 @@ var PYTHON_TAINTED_PATTERNS2 = [
|
|
|
25647
25751
|
// configs/sources/python.json but was not in the forward-taint regex
|
|
25648
25752
|
// registry, so `name = input()` was not added to pyTaintedVars. Closes
|
|
25649
25753
|
// the deferred `getattr(obj, input())()` reflection-invocation shape.
|
|
25650
|
-
{ pattern: /\binput\s*\(/, type: "io_input" }
|
|
25754
|
+
{ pattern: /\binput\s*\(/, type: "io_input" },
|
|
25755
|
+
// WebSocket transport channels (cognium-dev #213 second slice).
|
|
25756
|
+
// FastAPI / Starlette (`from fastapi import WebSocket`) and Django
|
|
25757
|
+
// Channels consumers both expose `receive_*` methods that return
|
|
25758
|
+
// untrusted frame payloads on every call. Registered in
|
|
25759
|
+
// config-loader.ts as return-tainted; the forward-taint regex here
|
|
25760
|
+
// enables `data = await websocket.receive_text()` → `data` tainted,
|
|
25761
|
+
// which the DFG-less Python path needs so downstream sinks that
|
|
25762
|
+
// consume `data` are flagged.
|
|
25763
|
+
//
|
|
25764
|
+
// Deliberately not adding a bare `.receive\s*\(` here — that would
|
|
25765
|
+
// match too many unrelated APIs (queue receive, signal receive, etc.)
|
|
25766
|
+
// and produce spurious sources on every `x = q.receive()` in the wild.
|
|
25767
|
+
{ pattern: /\.receive_text\s*\(/, type: "network_input" },
|
|
25768
|
+
{ pattern: /\.receive_bytes\s*\(/, type: "network_input" },
|
|
25769
|
+
{ pattern: /\.receive_json\s*\(/, type: "http_body" }
|
|
25651
25770
|
];
|
|
25652
25771
|
var LanguageSourcesPass = class {
|
|
25653
25772
|
name = "language-sources";
|
|
@@ -26339,6 +26458,31 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
26339
26458
|
}
|
|
26340
26459
|
}
|
|
26341
26460
|
}
|
|
26461
|
+
sources.push(...findJavaScriptCallbackParamSources(sourceCode));
|
|
26462
|
+
return sources;
|
|
26463
|
+
}
|
|
26464
|
+
function findJavaScriptCallbackParamSources(sourceCode) {
|
|
26465
|
+
const sources = [];
|
|
26466
|
+
const CB_EVENT_RE = /\.on\s*\(\s*['"](?:message|text|binary)['"]\s*,\s*(?:async\s+)?(?:function\s*\*?\s*(?:\w+\s*)?\(\s*(\w+)|\(\s*(\w+)\s*(?:[,):]|$)|(\w+)\s*=>)/;
|
|
26467
|
+
const lines = sourceCode.split("\n");
|
|
26468
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26469
|
+
const line = lines[i2];
|
|
26470
|
+
const trimmed = line.trimStart();
|
|
26471
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
|
|
26472
|
+
const m = line.match(CB_EVENT_RE);
|
|
26473
|
+
if (!m) continue;
|
|
26474
|
+
const paramName = m[1] ?? m[2] ?? m[3];
|
|
26475
|
+
if (!paramName) continue;
|
|
26476
|
+
if (paramName === "err" || paramName === "error") continue;
|
|
26477
|
+
sources.push({
|
|
26478
|
+
type: "network_input",
|
|
26479
|
+
location: `WebSocket .on(...) callback param '${paramName}' at line ${i2 + 1}`,
|
|
26480
|
+
severity: "high",
|
|
26481
|
+
line: i2 + 1,
|
|
26482
|
+
confidence: 0.95,
|
|
26483
|
+
variable: paramName
|
|
26484
|
+
});
|
|
26485
|
+
}
|
|
26342
26486
|
return sources;
|
|
26343
26487
|
}
|
|
26344
26488
|
function findPythonAssignmentSources(sourceCode, language) {
|
|
@@ -26621,6 +26765,14 @@ function buildJavaScriptTaintedVars(sourceCode, language) {
|
|
|
26621
26765
|
if (!["javascript", "typescript"].includes(language)) return /* @__PURE__ */ new Map();
|
|
26622
26766
|
const tainted = /* @__PURE__ */ new Map();
|
|
26623
26767
|
const lines = sourceCode.split("\n");
|
|
26768
|
+
const CB_EVENT_RE = /\.on\s*\(\s*['"](?:message|text|binary)['"]\s*,\s*(?:async\s+)?(?:function\s*\*?\s*(?:\w+\s*)?\(\s*(\w+)|\(\s*(\w+)\s*(?:[,):]|$)|(\w+)\s*=>)/;
|
|
26769
|
+
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26770
|
+
const m = lines[i2].match(CB_EVENT_RE);
|
|
26771
|
+
if (!m) continue;
|
|
26772
|
+
const paramName = m[1] ?? m[2] ?? m[3];
|
|
26773
|
+
if (!paramName || paramName === "err" || paramName === "error") continue;
|
|
26774
|
+
tainted.set(paramName, i2 + 1);
|
|
26775
|
+
}
|
|
26624
26776
|
for (let i2 = 0; i2 < lines.length; i2++) {
|
|
26625
26777
|
const line = lines[i2];
|
|
26626
26778
|
const trimmed = line.trimStart();
|
|
@@ -26817,6 +26969,79 @@ function findBashTaintSources(sourceCode, dfg) {
|
|
|
26817
26969
|
});
|
|
26818
26970
|
}
|
|
26819
26971
|
}
|
|
26972
|
+
const readMatch = /^read\s*\(/.test(trimmed) ? null : trimmed.match(/^read\b([^#(]*)$/);
|
|
26973
|
+
if (readMatch) {
|
|
26974
|
+
const argStr = readMatch[1].trim();
|
|
26975
|
+
const noRedirect = argStr.replace(/\s*<[^<].*$/, "").trim();
|
|
26976
|
+
const tokens = noRedirect.split(/\s+/);
|
|
26977
|
+
const varNames = [];
|
|
26978
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
26979
|
+
const tok = tokens[ti];
|
|
26980
|
+
if (!tok) continue;
|
|
26981
|
+
if (tok.startsWith("-")) {
|
|
26982
|
+
if (/^-[antpidu]$|^-N$/.test(tok)) ti++;
|
|
26983
|
+
continue;
|
|
26984
|
+
}
|
|
26985
|
+
if (/^[A-Za-z_][\w]*$/.test(tok)) varNames.push(tok);
|
|
26986
|
+
}
|
|
26987
|
+
if (varNames.length === 0) varNames.push("REPLY");
|
|
26988
|
+
for (const v of varNames) {
|
|
26989
|
+
const already = sources.some((s) => s.line === lineNumber && s.variable === v);
|
|
26990
|
+
if (already) continue;
|
|
26991
|
+
sources.push({
|
|
26992
|
+
type: "io_input",
|
|
26993
|
+
location: `read \u2192 $${v} (stdin)`,
|
|
26994
|
+
severity: "high",
|
|
26995
|
+
line: lineNumber,
|
|
26996
|
+
confidence: 0.9,
|
|
26997
|
+
variable: v
|
|
26998
|
+
});
|
|
26999
|
+
}
|
|
27000
|
+
}
|
|
27001
|
+
const mapfileMatch = /^(?:mapfile|readarray)\s*\(/.test(trimmed) ? null : trimmed.match(/^(?:mapfile|readarray)\b([^#(]*)$/);
|
|
27002
|
+
if (mapfileMatch) {
|
|
27003
|
+
const argStr = mapfileMatch[1].trim();
|
|
27004
|
+
const tokens = argStr.split(/\s+/);
|
|
27005
|
+
const varNames = [];
|
|
27006
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
27007
|
+
const tok = tokens[ti];
|
|
27008
|
+
if (!tok) continue;
|
|
27009
|
+
if (tok.startsWith("-")) {
|
|
27010
|
+
if (/^-[cCnOsud]$/.test(tok)) ti++;
|
|
27011
|
+
continue;
|
|
27012
|
+
}
|
|
27013
|
+
if (/^[A-Za-z_][\w]*$/.test(tok)) varNames.push(tok);
|
|
27014
|
+
}
|
|
27015
|
+
if (varNames.length === 0) varNames.push("MAPFILE");
|
|
27016
|
+
for (const v of varNames) {
|
|
27017
|
+
const already = sources.some((s) => s.line === lineNumber && s.variable === v);
|
|
27018
|
+
if (already) continue;
|
|
27019
|
+
sources.push({
|
|
27020
|
+
type: "io_input",
|
|
27021
|
+
location: `mapfile \u2192 $${v} (stdin array)`,
|
|
27022
|
+
severity: "high",
|
|
27023
|
+
line: lineNumber,
|
|
27024
|
+
confidence: 0.9,
|
|
27025
|
+
variable: v
|
|
27026
|
+
});
|
|
27027
|
+
}
|
|
27028
|
+
}
|
|
27029
|
+
const getoptsMatch = trimmed.match(/\bgetopts\s+["'][^"']+["']\s+(\w+)/);
|
|
27030
|
+
if (getoptsMatch) {
|
|
27031
|
+
const flagVar = getoptsMatch[1];
|
|
27032
|
+
for (const v of [flagVar, "OPTARG"]) {
|
|
27033
|
+
const already = sources.some((s) => s.line === lineNumber && s.variable === v);
|
|
27034
|
+
if (already) continue;
|
|
27035
|
+
sources.push({
|
|
27036
|
+
type: "io_input",
|
|
27037
|
+
location: `getopts \u2192 $${v} (CLI arg)`,
|
|
27038
|
+
severity: "high",
|
|
27039
|
+
line: lineNumber,
|
|
27040
|
+
confidence: 0.9,
|
|
27041
|
+
variable: v
|
|
27042
|
+
});
|
|
27043
|
+
}
|
|
27044
|
+
}
|
|
26820
27045
|
const envRe = /\$([A-Z][A-Z0-9_]{2,})|\$\{([A-Z][A-Z0-9_]{2,})\}/g;
|
|
26821
27046
|
let em;
|
|
26822
27047
|
while ((em = envRe.exec(line)) !== null) {
|
|
@@ -10721,6 +10721,49 @@ var DEFAULT_SOURCES = [
|
|
|
10721
10721
|
// Server-side interceptor receives `Metadata headers`; `headers.get(KEY)`
|
|
10722
10722
|
// returns caller-supplied header values.
|
|
10723
10723
|
{ method: "get", class: "Metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["java"] },
|
|
10724
|
+
// --- WebSocket transport channels (cognium-dev #213 second slice) ---
|
|
10725
|
+
//
|
|
10726
|
+
// Server-side WebSocket handlers receive attacker-authored frames on
|
|
10727
|
+
// every call to a receive-shaped method. Untrusted the moment they
|
|
10728
|
+
// return, regardless of any application-level auth on the socket.
|
|
10729
|
+
//
|
|
10730
|
+
// Python — FastAPI / Starlette (`from fastapi import WebSocket`):
|
|
10731
|
+
// data = await websocket.receive_text()
|
|
10732
|
+
// data = await websocket.receive_bytes()
|
|
10733
|
+
// data = await websocket.receive_json() # parsed dict/list
|
|
10734
|
+
// data = await websocket.receive() # {'type', 'text'|'bytes'}
|
|
10735
|
+
{ method: "receive_text", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10736
|
+
{ method: "receive_bytes", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10737
|
+
{ method: "receive_json", class: "WebSocket", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10738
|
+
//
|
|
10739
|
+
// `receive` intentionally class-scoped to WebSocket (Starlette pattern).
|
|
10740
|
+
// Broadening to unqualified would collide with queue/signal `.receive()`.
|
|
10741
|
+
{ method: "receive", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10742
|
+
// Django Channels `AsyncJsonWebsocketConsumer` / `WebsocketConsumer`
|
|
10743
|
+
// expose the same receive_* names on `self`; the class filter matches
|
|
10744
|
+
// `WebSocket` only, so add unqualified fallbacks for the receive_json /
|
|
10745
|
+
// receive_text convention (broad — no class filter possible without
|
|
10746
|
+
// hardcoding Django's consumer names).
|
|
10747
|
+
{ method: "receive_json", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10748
|
+
{ method: "receive_text", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10749
|
+
{ method: "receive_bytes", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10750
|
+
// Go — gorilla/websocket (`github.com/gorilla/websocket`):
|
|
10751
|
+
// messageType, message, err := conn.ReadMessage()
|
|
10752
|
+
// _, r, err := conn.NextReader()
|
|
10753
|
+
{ method: "ReadMessage", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
10754
|
+
{ method: "NextReader", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
10755
|
+
// nhooyr.io/websocket exposes a `Read` method on `*websocket.Conn`;
|
|
10756
|
+
// signature is `func (c *Conn) Read(ctx) (MessageType, []byte, error)`.
|
|
10757
|
+
{ method: "Read", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
10758
|
+
// Java — Jakarta / Java WebSocket API (`javax.websocket` / `jakarta.websocket`):
|
|
10759
|
+
// session.getBasicRemote().sendText(input); // OUT (not a source)
|
|
10760
|
+
// @OnMessage public void onMessage(String msg) — `msg` is a callback
|
|
10761
|
+
// param, not a return; handled via param_tainted when annotation is set.
|
|
10762
|
+
{ annotation: "OnMessage", type: "network_input", severity: "high", param_tainted: true, languages: ["java"] },
|
|
10763
|
+
// Spring `@MessageMapping` STOMP-over-WebSocket handlers receive
|
|
10764
|
+
// untrusted payloads on every dispatched frame.
|
|
10765
|
+
{ annotation: "MessageMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
|
|
10766
|
+
{ annotation: "SubscribeMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
|
|
10724
10767
|
// --- Cache reads (second-order taint — Redis / Memcached / Django cache) ---
|
|
10725
10768
|
// The cache round-trip is a canonical second-order sink: whatever was
|
|
10726
10769
|
// written previously (potentially attacker-controlled) resurfaces on read.
|
|
@@ -12793,6 +12836,60 @@ var DEFAULT_SANITIZERS = [
|
|
|
12793
12836
|
// Python Type coercion
|
|
12794
12837
|
{ method: "int", removes: ["sql_injection", "command_injection", "xss"] },
|
|
12795
12838
|
{ method: "float", removes: ["sql_injection", "command_injection"] },
|
|
12839
|
+
// Python URL encoding (cognium-dev #213 fifth slice).
|
|
12840
|
+
//
|
|
12841
|
+
// urllib.parse.quote(x) — RFC-3986 percent-encode; safe for URL path
|
|
12842
|
+
// urllib.parse.quote_plus(x) — same + space→+; safe for query string
|
|
12843
|
+
// urllib.parse.urlencode(d) — encode a dict of pairs
|
|
12844
|
+
//
|
|
12845
|
+
// Bounded to URL-context sinks. Class-scoped to the specific
|
|
12846
|
+
// `urllib.parse` receiver to avoid colliding with unrelated bare
|
|
12847
|
+
// `quote(...)` calls in other libraries.
|
|
12848
|
+
{ method: "quote", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
12849
|
+
{ method: "quote_plus", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
12850
|
+
{ method: "urlencode", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss"] },
|
|
12851
|
+
// Bare aliases — `from urllib.parse import quote` then unqualified.
|
|
12852
|
+
{ method: "quote_plus", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
12853
|
+
{ method: "urlencode", removes: ["ssrf", "open_redirect", "xss"] },
|
|
12854
|
+
// `quote` bare is intentionally NOT registered — it collides with
|
|
12855
|
+
// shlex.quote (bare-imported) which is a command_injection sanitizer,
|
|
12856
|
+
// not a URL sanitizer. The class-scoped variant above catches the
|
|
12857
|
+
// qualified `urllib.parse.quote(...)` shape; unqualified callers who
|
|
12858
|
+
// want URL-context credit should use `quote_plus` (which does not
|
|
12859
|
+
// collide with any command_injection sanitizer).
|
|
12860
|
+
// Python XSS — additional common sanitizers.
|
|
12861
|
+
//
|
|
12862
|
+
// bleach.clean(...) — already covered above (line 2898)
|
|
12863
|
+
// bleach.linkify(...) — turns URLs into anchor tags; sanitizes as well
|
|
12864
|
+
// django.utils.html.escape / strip_tags — Django's XSS escape helpers
|
|
12865
|
+
// jinja2.escape — Jinja2's Markup escape (aliased from markupsafe)
|
|
12866
|
+
// flask.escape — Flask re-export of markupsafe.escape
|
|
12867
|
+
// saxutils.escape — stdlib xml.sax.saxutils.escape for XML docs
|
|
12868
|
+
{ method: "linkify", class: "bleach", removes: ["xss"] },
|
|
12869
|
+
// Bare `linkify(...)` alias — `from bleach import linkify`.
|
|
12870
|
+
{ method: "linkify", removes: ["xss"] },
|
|
12871
|
+
{ method: "escape", class: "django.utils.html", removes: ["xss"] },
|
|
12872
|
+
{ method: "strip_tags", class: "django.utils.html", removes: ["xss"] },
|
|
12873
|
+
{ method: "escape", class: "jinja2", removes: ["xss"] },
|
|
12874
|
+
{ method: "escape", class: "flask", removes: ["xss"] },
|
|
12875
|
+
{ method: "escape", class: "saxutils", removes: ["xss"] },
|
|
12876
|
+
{ method: "escape", class: "xml.sax.saxutils", removes: ["xss"] },
|
|
12877
|
+
{ method: "quoteattr", class: "saxutils", removes: ["xss"] },
|
|
12878
|
+
{ method: "quoteattr", class: "xml.sax.saxutils", removes: ["xss"] },
|
|
12879
|
+
// Python ReDoS — `re.escape(user)` when building a regex from user input
|
|
12880
|
+
// strips regex metacharacters. Downstream `re.compile / re.match` cannot
|
|
12881
|
+
// interpret user-supplied alternations or quantifiers. Also covers the
|
|
12882
|
+
// `code_injection` categorization that `re.compile` currently emits
|
|
12883
|
+
// (a re.escape-wrapped pattern cannot execute anything, so both are safe).
|
|
12884
|
+
{ method: "escape", class: "re", removes: ["redos", "code_injection"] },
|
|
12885
|
+
// Python SQLAlchemy — `text(...).bindparams(...)` binds params safely.
|
|
12886
|
+
// The `bindparams` call is the sanitizer; the parent `text` wraps the
|
|
12887
|
+
// template. Also add `expression.literal` for explicit SQL literals.
|
|
12888
|
+
{ method: "bindparams", removes: ["sql_injection"] },
|
|
12889
|
+
// psycopg2 sql-composition helpers
|
|
12890
|
+
{ method: "Identifier", class: "sql", removes: ["sql_injection"] },
|
|
12891
|
+
{ method: "Literal", class: "sql", removes: ["sql_injection"] },
|
|
12892
|
+
{ method: "Placeholder", class: "sql", removes: ["sql_injection"] },
|
|
12796
12893
|
// =========================================================================
|
|
12797
12894
|
// Rust Sanitizers
|
|
12798
12895
|
// =========================================================================
|
|
@@ -14621,7 +14718,14 @@ function matchesSanitizerPattern(call, pattern) {
|
|
|
14621
14718
|
return false;
|
|
14622
14719
|
}
|
|
14623
14720
|
if (pattern.class) {
|
|
14624
|
-
if (!call.receiver
|
|
14721
|
+
if (!call.receiver) {
|
|
14722
|
+
const target = call.resolution?.target;
|
|
14723
|
+
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
14724
|
+
if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
|
|
14725
|
+
} else {
|
|
14726
|
+
return false;
|
|
14727
|
+
}
|
|
14728
|
+
} else if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
14625
14729
|
return false;
|
|
14626
14730
|
}
|
|
14627
14731
|
}
|
|
@@ -10655,6 +10655,49 @@ var DEFAULT_SOURCES = [
|
|
|
10655
10655
|
// Server-side interceptor receives `Metadata headers`; `headers.get(KEY)`
|
|
10656
10656
|
// returns caller-supplied header values.
|
|
10657
10657
|
{ method: "get", class: "Metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["java"] },
|
|
10658
|
+
// --- WebSocket transport channels (cognium-dev #213 second slice) ---
|
|
10659
|
+
//
|
|
10660
|
+
// Server-side WebSocket handlers receive attacker-authored frames on
|
|
10661
|
+
// every call to a receive-shaped method. Untrusted the moment they
|
|
10662
|
+
// return, regardless of any application-level auth on the socket.
|
|
10663
|
+
//
|
|
10664
|
+
// Python — FastAPI / Starlette (`from fastapi import WebSocket`):
|
|
10665
|
+
// data = await websocket.receive_text()
|
|
10666
|
+
// data = await websocket.receive_bytes()
|
|
10667
|
+
// data = await websocket.receive_json() # parsed dict/list
|
|
10668
|
+
// data = await websocket.receive() # {'type', 'text'|'bytes'}
|
|
10669
|
+
{ method: "receive_text", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10670
|
+
{ method: "receive_bytes", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10671
|
+
{ method: "receive_json", class: "WebSocket", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10672
|
+
//
|
|
10673
|
+
// `receive` intentionally class-scoped to WebSocket (Starlette pattern).
|
|
10674
|
+
// Broadening to unqualified would collide with queue/signal `.receive()`.
|
|
10675
|
+
{ method: "receive", class: "WebSocket", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10676
|
+
// Django Channels `AsyncJsonWebsocketConsumer` / `WebsocketConsumer`
|
|
10677
|
+
// expose the same receive_* names on `self`; the class filter matches
|
|
10678
|
+
// `WebSocket` only, so add unqualified fallbacks for the receive_json /
|
|
10679
|
+
// receive_text convention (broad — no class filter possible without
|
|
10680
|
+
// hardcoding Django's consumer names).
|
|
10681
|
+
{ method: "receive_json", type: "http_body", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10682
|
+
{ method: "receive_text", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10683
|
+
{ method: "receive_bytes", type: "network_input", severity: "high", return_tainted: true, languages: ["python"] },
|
|
10684
|
+
// Go — gorilla/websocket (`github.com/gorilla/websocket`):
|
|
10685
|
+
// messageType, message, err := conn.ReadMessage()
|
|
10686
|
+
// _, r, err := conn.NextReader()
|
|
10687
|
+
{ method: "ReadMessage", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
10688
|
+
{ method: "NextReader", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
10689
|
+
// nhooyr.io/websocket exposes a `Read` method on `*websocket.Conn`;
|
|
10690
|
+
// signature is `func (c *Conn) Read(ctx) (MessageType, []byte, error)`.
|
|
10691
|
+
{ method: "Read", class: "Conn", type: "network_input", severity: "high", return_tainted: true, languages: ["go"] },
|
|
10692
|
+
// Java — Jakarta / Java WebSocket API (`javax.websocket` / `jakarta.websocket`):
|
|
10693
|
+
// session.getBasicRemote().sendText(input); // OUT (not a source)
|
|
10694
|
+
// @OnMessage public void onMessage(String msg) — `msg` is a callback
|
|
10695
|
+
// param, not a return; handled via param_tainted when annotation is set.
|
|
10696
|
+
{ annotation: "OnMessage", type: "network_input", severity: "high", param_tainted: true, languages: ["java"] },
|
|
10697
|
+
// Spring `@MessageMapping` STOMP-over-WebSocket handlers receive
|
|
10698
|
+
// untrusted payloads on every dispatched frame.
|
|
10699
|
+
{ annotation: "MessageMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
|
|
10700
|
+
{ annotation: "SubscribeMapping", type: "http_body", severity: "high", param_tainted: true, languages: ["java"] },
|
|
10658
10701
|
// --- Cache reads (second-order taint — Redis / Memcached / Django cache) ---
|
|
10659
10702
|
// The cache round-trip is a canonical second-order sink: whatever was
|
|
10660
10703
|
// written previously (potentially attacker-controlled) resurfaces on read.
|
|
@@ -12727,6 +12770,60 @@ var DEFAULT_SANITIZERS = [
|
|
|
12727
12770
|
// Python Type coercion
|
|
12728
12771
|
{ method: "int", removes: ["sql_injection", "command_injection", "xss"] },
|
|
12729
12772
|
{ method: "float", removes: ["sql_injection", "command_injection"] },
|
|
12773
|
+
// Python URL encoding (cognium-dev #213 fifth slice).
|
|
12774
|
+
//
|
|
12775
|
+
// urllib.parse.quote(x) — RFC-3986 percent-encode; safe for URL path
|
|
12776
|
+
// urllib.parse.quote_plus(x) — same + space→+; safe for query string
|
|
12777
|
+
// urllib.parse.urlencode(d) — encode a dict of pairs
|
|
12778
|
+
//
|
|
12779
|
+
// Bounded to URL-context sinks. Class-scoped to the specific
|
|
12780
|
+
// `urllib.parse` receiver to avoid colliding with unrelated bare
|
|
12781
|
+
// `quote(...)` calls in other libraries.
|
|
12782
|
+
{ method: "quote", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
12783
|
+
{ method: "quote_plus", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
12784
|
+
{ method: "urlencode", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss"] },
|
|
12785
|
+
// Bare aliases — `from urllib.parse import quote` then unqualified.
|
|
12786
|
+
{ method: "quote_plus", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
|
|
12787
|
+
{ method: "urlencode", removes: ["ssrf", "open_redirect", "xss"] },
|
|
12788
|
+
// `quote` bare is intentionally NOT registered — it collides with
|
|
12789
|
+
// shlex.quote (bare-imported) which is a command_injection sanitizer,
|
|
12790
|
+
// not a URL sanitizer. The class-scoped variant above catches the
|
|
12791
|
+
// qualified `urllib.parse.quote(...)` shape; unqualified callers who
|
|
12792
|
+
// want URL-context credit should use `quote_plus` (which does not
|
|
12793
|
+
// collide with any command_injection sanitizer).
|
|
12794
|
+
// Python XSS — additional common sanitizers.
|
|
12795
|
+
//
|
|
12796
|
+
// bleach.clean(...) — already covered above (line 2898)
|
|
12797
|
+
// bleach.linkify(...) — turns URLs into anchor tags; sanitizes as well
|
|
12798
|
+
// django.utils.html.escape / strip_tags — Django's XSS escape helpers
|
|
12799
|
+
// jinja2.escape — Jinja2's Markup escape (aliased from markupsafe)
|
|
12800
|
+
// flask.escape — Flask re-export of markupsafe.escape
|
|
12801
|
+
// saxutils.escape — stdlib xml.sax.saxutils.escape for XML docs
|
|
12802
|
+
{ method: "linkify", class: "bleach", removes: ["xss"] },
|
|
12803
|
+
// Bare `linkify(...)` alias — `from bleach import linkify`.
|
|
12804
|
+
{ method: "linkify", removes: ["xss"] },
|
|
12805
|
+
{ method: "escape", class: "django.utils.html", removes: ["xss"] },
|
|
12806
|
+
{ method: "strip_tags", class: "django.utils.html", removes: ["xss"] },
|
|
12807
|
+
{ method: "escape", class: "jinja2", removes: ["xss"] },
|
|
12808
|
+
{ method: "escape", class: "flask", removes: ["xss"] },
|
|
12809
|
+
{ method: "escape", class: "saxutils", removes: ["xss"] },
|
|
12810
|
+
{ method: "escape", class: "xml.sax.saxutils", removes: ["xss"] },
|
|
12811
|
+
{ method: "quoteattr", class: "saxutils", removes: ["xss"] },
|
|
12812
|
+
{ method: "quoteattr", class: "xml.sax.saxutils", removes: ["xss"] },
|
|
12813
|
+
// Python ReDoS — `re.escape(user)` when building a regex from user input
|
|
12814
|
+
// strips regex metacharacters. Downstream `re.compile / re.match` cannot
|
|
12815
|
+
// interpret user-supplied alternations or quantifiers. Also covers the
|
|
12816
|
+
// `code_injection` categorization that `re.compile` currently emits
|
|
12817
|
+
// (a re.escape-wrapped pattern cannot execute anything, so both are safe).
|
|
12818
|
+
{ method: "escape", class: "re", removes: ["redos", "code_injection"] },
|
|
12819
|
+
// Python SQLAlchemy — `text(...).bindparams(...)` binds params safely.
|
|
12820
|
+
// The `bindparams` call is the sanitizer; the parent `text` wraps the
|
|
12821
|
+
// template. Also add `expression.literal` for explicit SQL literals.
|
|
12822
|
+
{ method: "bindparams", removes: ["sql_injection"] },
|
|
12823
|
+
// psycopg2 sql-composition helpers
|
|
12824
|
+
{ method: "Identifier", class: "sql", removes: ["sql_injection"] },
|
|
12825
|
+
{ method: "Literal", class: "sql", removes: ["sql_injection"] },
|
|
12826
|
+
{ method: "Placeholder", class: "sql", removes: ["sql_injection"] },
|
|
12730
12827
|
// =========================================================================
|
|
12731
12828
|
// Rust Sanitizers
|
|
12732
12829
|
// =========================================================================
|
|
@@ -14555,7 +14652,14 @@ function matchesSanitizerPattern(call, pattern) {
|
|
|
14555
14652
|
return false;
|
|
14556
14653
|
}
|
|
14557
14654
|
if (pattern.class) {
|
|
14558
|
-
if (!call.receiver
|
|
14655
|
+
if (!call.receiver) {
|
|
14656
|
+
const target = call.resolution?.target;
|
|
14657
|
+
const expectedTail = `${pattern.class}.${pattern.method}`;
|
|
14658
|
+
if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
|
|
14659
|
+
} else {
|
|
14660
|
+
return false;
|
|
14661
|
+
}
|
|
14662
|
+
} else if (!receiverMightBeClass(call.receiver, pattern.class)) {
|
|
14559
14663
|
return false;
|
|
14560
14664
|
}
|
|
14561
14665
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circle-ir",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.185.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",
|