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.
@@ -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 || !receiverMightBeClass(call.receiver, pattern.class)) {
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 || !receiverMightBeClass(call.receiver, pattern.class)) {
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 || !receiverMightBeClass(call.receiver, pattern.class)) {
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.182.0",
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",