circle-ir 3.182.0 → 3.186.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.
@@ -13354,6 +13397,91 @@ var DEFAULT_SANITIZERS = [
13354
13397
  // Command injection - shell escaping
13355
13398
  { method: "quote", class: "shell", removes: ["command_injection"] },
13356
13399
  { method: "escape", class: "shell-escape", removes: ["command_injection"] },
13400
+ // JS/Node — additional XSS/HTML encoders (cognium-dev #213 sixth slice).
13401
+ //
13402
+ // he.encode(x) / he.escape(x) — `he` npm package
13403
+ // sanitizeHtml(x) — `sanitize-html` npm
13404
+ // xss(x) / filterXSS(x) — `xss` npm (Yahoo)
13405
+ // escapeHtml(x) / escapeHTML(x) — common bare helpers
13406
+ // entities.encode(x) / entities.escape(x) — `entities` npm
13407
+ // xssFilters.inHTMLData(x) / inHTMLComment(x) / uriInHTMLData(x)
13408
+ // — `xss-filters` npm (Yahoo)
13409
+ //
13410
+ // `external_taint_escape` is included alongside `xss` — the CWE-668
13411
+ // fallback fires on any call receiving tainted data that the engine
13412
+ // does not otherwise recognize. Since these functions are explicitly
13413
+ // registered as sanitizers (they PURIFY input), they should also
13414
+ // suppress the fallback so a `res.send(sanitizeHtml(x))` flow does
13415
+ // not report an external-taint-escape at the sanitizer call itself.
13416
+ { method: "encode", class: "he", removes: ["xss", "external_taint_escape"] },
13417
+ { method: "escape", class: "he", removes: ["xss", "external_taint_escape"] },
13418
+ { method: "sanitizeHtml", removes: ["xss", "external_taint_escape"] },
13419
+ { method: "xss", removes: ["xss", "external_taint_escape"] },
13420
+ { method: "filterXSS", removes: ["xss", "external_taint_escape"] },
13421
+ { method: "escapeHtml", removes: ["xss", "external_taint_escape"] },
13422
+ { method: "escapeHTML", removes: ["xss", "external_taint_escape"] },
13423
+ { method: "encode", class: "entities", removes: ["xss", "external_taint_escape"] },
13424
+ { method: "escape", class: "entities", removes: ["xss", "external_taint_escape"] },
13425
+ // xss-filters: context-specific helpers, all XSS-safe by construction.
13426
+ { method: "inHTMLData", class: "xssFilters", removes: ["xss", "external_taint_escape"] },
13427
+ { method: "inHTMLComment", class: "xssFilters", removes: ["xss", "external_taint_escape"] },
13428
+ { method: "uriInHTMLData", class: "xssFilters", removes: ["xss", "external_taint_escape"] },
13429
+ // JS/Node — SQL escaping (in addition to the mysql class above).
13430
+ //
13431
+ // SqlString.escape / .escapeId — `sqlstring` npm (raw
13432
+ // escape used by mysql /
13433
+ // node-mysql2 drivers)
13434
+ // pgFormat(sql, ...args) / format(bare) — `pg-format` npm
13435
+ // SQL.raw / SQL(bare) — `sql-template-strings`
13436
+ //
13437
+ // Bare `format` is intentionally NOT registered — it collides with
13438
+ // string.format-style helpers unrelated to SQL. Callers should use the
13439
+ // qualified form.
13440
+ { method: "escape", class: "SqlString", removes: ["sql_injection", "external_taint_escape"] },
13441
+ { method: "escapeId", class: "SqlString", removes: ["sql_injection", "external_taint_escape"] },
13442
+ { method: "format", class: "SqlString", removes: ["sql_injection", "external_taint_escape"] },
13443
+ { method: "format", class: "pgFormat", removes: ["sql_injection", "external_taint_escape"] },
13444
+ { method: "ident", class: "pgFormat", removes: ["sql_injection", "external_taint_escape"] },
13445
+ { method: "literal", class: "pgFormat", removes: ["sql_injection", "external_taint_escape"] },
13446
+ // JS/Node — crypto.randomUUID() and Node's crypto.randomBytes(...)toString()
13447
+ // produce cryptographically-secure typed strings that cannot carry
13448
+ // attacker-injected payload. Registered as type coercion.
13449
+ { method: "randomUUID", class: "crypto", removes: ["sql_injection", "nosql_injection", "command_injection", "path_traversal", "code_injection", "xss"] },
13450
+ // Bare `randomUUID()` alias — `import { randomUUID } from 'crypto'`.
13451
+ { method: "randomUUID", removes: ["sql_injection", "nosql_injection", "command_injection", "path_traversal", "code_injection", "xss"] },
13452
+ // JS/Node — URL construction. `new URL(x, base)` (constructor-call
13453
+ // matched by method_name === 'URL') and `URLSearchParams.toString()`
13454
+ // safely encode components for URL context.
13455
+ { method: "toString", class: "URLSearchParams", removes: ["ssrf", "open_redirect", "xss"] },
13456
+ // Go — additional sanitizers (cognium-dev #213 sixth slice).
13457
+ //
13458
+ // regexp.QuoteMeta(x) — mirror of Python re.escape; strips regex
13459
+ // metacharacters so downstream compile/match
13460
+ // cannot execute attacker-crafted patterns.
13461
+ // bluemonday.Sanitize — dominant Go HTML sanitizer library
13462
+ // (`microcosm-cc/bluemonday`). Called on a
13463
+ // Policy value returned by UGCPolicy() /
13464
+ // StrictPolicy() / etc. Match by method name
13465
+ // alone — Policy is the receiver type.
13466
+ // net.ParseIP(x) — validates the input is a well-formed IP;
13467
+ // returns nil for bad input (developer must
13468
+ // check, but the sanitizer only applies when
13469
+ // the parsed IP flows onward).
13470
+ // sql.Named(name, val) — parameterized query binding.
13471
+ { method: "QuoteMeta", class: "regexp", removes: ["redos", "code_injection", "external_taint_escape"] },
13472
+ { method: "Sanitize", class: "bluemonday", removes: ["xss", "external_taint_escape"] },
13473
+ { method: "Sanitize", class: "Policy", removes: ["xss", "external_taint_escape"] },
13474
+ // Bare `Sanitize` — the receiver is a Policy variable (`p := bluemonday.UGCPolicy(); p.Sanitize(x)`).
13475
+ // `Policy` class-matching only fires when the IR resolves the receiver type;
13476
+ // since Go DFG rarely propagates the type of a locally-assigned variable
13477
+ // from a package factory call, we also register the bare method name.
13478
+ // Narrow FP risk: `Sanitize` bare is uncommon outside bluemonday context.
13479
+ { method: "Sanitize", removes: ["xss", "external_taint_escape"] },
13480
+ { method: "SanitizeBytes", class: "bluemonday", removes: ["xss", "external_taint_escape"] },
13481
+ { method: "SanitizeBytes", class: "Policy", removes: ["xss", "external_taint_escape"] },
13482
+ { method: "SanitizeBytes", removes: ["xss", "external_taint_escape"] },
13483
+ { method: "ParseIP", class: "net", removes: ["ssrf", "command_injection", "path_traversal", "external_taint_escape"] },
13484
+ { method: "Named", class: "sql", removes: ["sql_injection", "external_taint_escape"] },
13357
13485
  // =========================================================================
13358
13486
  // Python Sanitizers
13359
13487
  // =========================================================================
@@ -13398,6 +13526,60 @@ var DEFAULT_SANITIZERS = [
13398
13526
  // Python Type coercion
13399
13527
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
13400
13528
  { method: "float", removes: ["sql_injection", "command_injection"] },
13529
+ // Python URL encoding (cognium-dev #213 fifth slice).
13530
+ //
13531
+ // urllib.parse.quote(x) — RFC-3986 percent-encode; safe for URL path
13532
+ // urllib.parse.quote_plus(x) — same + space→+; safe for query string
13533
+ // urllib.parse.urlencode(d) — encode a dict of pairs
13534
+ //
13535
+ // Bounded to URL-context sinks. Class-scoped to the specific
13536
+ // `urllib.parse` receiver to avoid colliding with unrelated bare
13537
+ // `quote(...)` calls in other libraries.
13538
+ { method: "quote", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
13539
+ { method: "quote_plus", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
13540
+ { method: "urlencode", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss"] },
13541
+ // Bare aliases — `from urllib.parse import quote` then unqualified.
13542
+ { method: "quote_plus", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
13543
+ { method: "urlencode", removes: ["ssrf", "open_redirect", "xss"] },
13544
+ // `quote` bare is intentionally NOT registered — it collides with
13545
+ // shlex.quote (bare-imported) which is a command_injection sanitizer,
13546
+ // not a URL sanitizer. The class-scoped variant above catches the
13547
+ // qualified `urllib.parse.quote(...)` shape; unqualified callers who
13548
+ // want URL-context credit should use `quote_plus` (which does not
13549
+ // collide with any command_injection sanitizer).
13550
+ // Python XSS — additional common sanitizers.
13551
+ //
13552
+ // bleach.clean(...) — already covered above (line 2898)
13553
+ // bleach.linkify(...) — turns URLs into anchor tags; sanitizes as well
13554
+ // django.utils.html.escape / strip_tags — Django's XSS escape helpers
13555
+ // jinja2.escape — Jinja2's Markup escape (aliased from markupsafe)
13556
+ // flask.escape — Flask re-export of markupsafe.escape
13557
+ // saxutils.escape — stdlib xml.sax.saxutils.escape for XML docs
13558
+ { method: "linkify", class: "bleach", removes: ["xss"] },
13559
+ // Bare `linkify(...)` alias — `from bleach import linkify`.
13560
+ { method: "linkify", removes: ["xss"] },
13561
+ { method: "escape", class: "django.utils.html", removes: ["xss"] },
13562
+ { method: "strip_tags", class: "django.utils.html", removes: ["xss"] },
13563
+ { method: "escape", class: "jinja2", removes: ["xss"] },
13564
+ { method: "escape", class: "flask", removes: ["xss"] },
13565
+ { method: "escape", class: "saxutils", removes: ["xss"] },
13566
+ { method: "escape", class: "xml.sax.saxutils", removes: ["xss"] },
13567
+ { method: "quoteattr", class: "saxutils", removes: ["xss"] },
13568
+ { method: "quoteattr", class: "xml.sax.saxutils", removes: ["xss"] },
13569
+ // Python ReDoS — `re.escape(user)` when building a regex from user input
13570
+ // strips regex metacharacters. Downstream `re.compile / re.match` cannot
13571
+ // interpret user-supplied alternations or quantifiers. Also covers the
13572
+ // `code_injection` categorization that `re.compile` currently emits
13573
+ // (a re.escape-wrapped pattern cannot execute anything, so both are safe).
13574
+ { method: "escape", class: "re", removes: ["redos", "code_injection"] },
13575
+ // Python SQLAlchemy — `text(...).bindparams(...)` binds params safely.
13576
+ // The `bindparams` call is the sanitizer; the parent `text` wraps the
13577
+ // template. Also add `expression.literal` for explicit SQL literals.
13578
+ { method: "bindparams", removes: ["sql_injection"] },
13579
+ // psycopg2 sql-composition helpers
13580
+ { method: "Identifier", class: "sql", removes: ["sql_injection"] },
13581
+ { method: "Literal", class: "sql", removes: ["sql_injection"] },
13582
+ { method: "Placeholder", class: "sql", removes: ["sql_injection"] },
13401
13583
  // =========================================================================
13402
13584
  // Rust Sanitizers
13403
13585
  // =========================================================================
@@ -15321,7 +15503,14 @@ function matchesSanitizerPattern(call, pattern) {
15321
15503
  return false;
15322
15504
  }
15323
15505
  if (pattern.class) {
15324
- if (!call.receiver || !receiverMightBeClass(call.receiver, pattern.class)) {
15506
+ if (!call.receiver) {
15507
+ const target = call.resolution?.target;
15508
+ const expectedTail = `${pattern.class}.${pattern.method}`;
15509
+ if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
15510
+ } else {
15511
+ return false;
15512
+ }
15513
+ } else if (!receiverMightBeClass(call.receiver, pattern.class)) {
15325
15514
  return false;
15326
15515
  }
15327
15516
  }
@@ -25647,7 +25836,22 @@ var PYTHON_TAINTED_PATTERNS2 = [
25647
25836
  // configs/sources/python.json but was not in the forward-taint regex
25648
25837
  // registry, so `name = input()` was not added to pyTaintedVars. Closes
25649
25838
  // the deferred `getattr(obj, input())()` reflection-invocation shape.
25650
- { pattern: /\binput\s*\(/, type: "io_input" }
25839
+ { pattern: /\binput\s*\(/, type: "io_input" },
25840
+ // WebSocket transport channels (cognium-dev #213 second slice).
25841
+ // FastAPI / Starlette (`from fastapi import WebSocket`) and Django
25842
+ // Channels consumers both expose `receive_*` methods that return
25843
+ // untrusted frame payloads on every call. Registered in
25844
+ // config-loader.ts as return-tainted; the forward-taint regex here
25845
+ // enables `data = await websocket.receive_text()` → `data` tainted,
25846
+ // which the DFG-less Python path needs so downstream sinks that
25847
+ // consume `data` are flagged.
25848
+ //
25849
+ // Deliberately not adding a bare `.receive\s*\(` here — that would
25850
+ // match too many unrelated APIs (queue receive, signal receive, etc.)
25851
+ // and produce spurious sources on every `x = q.receive()` in the wild.
25852
+ { pattern: /\.receive_text\s*\(/, type: "network_input" },
25853
+ { pattern: /\.receive_bytes\s*\(/, type: "network_input" },
25854
+ { pattern: /\.receive_json\s*\(/, type: "http_body" }
25651
25855
  ];
25652
25856
  var LanguageSourcesPass = class {
25653
25857
  name = "language-sources";
@@ -26339,6 +26543,31 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
26339
26543
  }
26340
26544
  }
26341
26545
  }
26546
+ sources.push(...findJavaScriptCallbackParamSources(sourceCode));
26547
+ return sources;
26548
+ }
26549
+ function findJavaScriptCallbackParamSources(sourceCode) {
26550
+ const sources = [];
26551
+ 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*=>)/;
26552
+ const lines = sourceCode.split("\n");
26553
+ for (let i2 = 0; i2 < lines.length; i2++) {
26554
+ const line = lines[i2];
26555
+ const trimmed = line.trimStart();
26556
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
26557
+ const m = line.match(CB_EVENT_RE);
26558
+ if (!m) continue;
26559
+ const paramName = m[1] ?? m[2] ?? m[3];
26560
+ if (!paramName) continue;
26561
+ if (paramName === "err" || paramName === "error") continue;
26562
+ sources.push({
26563
+ type: "network_input",
26564
+ location: `WebSocket .on(...) callback param '${paramName}' at line ${i2 + 1}`,
26565
+ severity: "high",
26566
+ line: i2 + 1,
26567
+ confidence: 0.95,
26568
+ variable: paramName
26569
+ });
26570
+ }
26342
26571
  return sources;
26343
26572
  }
26344
26573
  function findPythonAssignmentSources(sourceCode, language) {
@@ -26621,6 +26850,14 @@ function buildJavaScriptTaintedVars(sourceCode, language) {
26621
26850
  if (!["javascript", "typescript"].includes(language)) return /* @__PURE__ */ new Map();
26622
26851
  const tainted = /* @__PURE__ */ new Map();
26623
26852
  const lines = sourceCode.split("\n");
26853
+ 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*=>)/;
26854
+ for (let i2 = 0; i2 < lines.length; i2++) {
26855
+ const m = lines[i2].match(CB_EVENT_RE);
26856
+ if (!m) continue;
26857
+ const paramName = m[1] ?? m[2] ?? m[3];
26858
+ if (!paramName || paramName === "err" || paramName === "error") continue;
26859
+ tainted.set(paramName, i2 + 1);
26860
+ }
26624
26861
  for (let i2 = 0; i2 < lines.length; i2++) {
26625
26862
  const line = lines[i2];
26626
26863
  const trimmed = line.trimStart();
@@ -26817,6 +27054,79 @@ function findBashTaintSources(sourceCode, dfg) {
26817
27054
  });
26818
27055
  }
26819
27056
  }
27057
+ const readMatch = /^read\s*\(/.test(trimmed) ? null : trimmed.match(/^read\b([^#(]*)$/);
27058
+ if (readMatch) {
27059
+ const argStr = readMatch[1].trim();
27060
+ const noRedirect = argStr.replace(/\s*<[^<].*$/, "").trim();
27061
+ const tokens = noRedirect.split(/\s+/);
27062
+ const varNames = [];
27063
+ for (let ti = 0; ti < tokens.length; ti++) {
27064
+ const tok = tokens[ti];
27065
+ if (!tok) continue;
27066
+ if (tok.startsWith("-")) {
27067
+ if (/^-[antpidu]$|^-N$/.test(tok)) ti++;
27068
+ continue;
27069
+ }
27070
+ if (/^[A-Za-z_][\w]*$/.test(tok)) varNames.push(tok);
27071
+ }
27072
+ if (varNames.length === 0) varNames.push("REPLY");
27073
+ for (const v of varNames) {
27074
+ const already = sources.some((s) => s.line === lineNumber && s.variable === v);
27075
+ if (already) continue;
27076
+ sources.push({
27077
+ type: "io_input",
27078
+ location: `read \u2192 $${v} (stdin)`,
27079
+ severity: "high",
27080
+ line: lineNumber,
27081
+ confidence: 0.9,
27082
+ variable: v
27083
+ });
27084
+ }
27085
+ }
27086
+ const mapfileMatch = /^(?:mapfile|readarray)\s*\(/.test(trimmed) ? null : trimmed.match(/^(?:mapfile|readarray)\b([^#(]*)$/);
27087
+ if (mapfileMatch) {
27088
+ const argStr = mapfileMatch[1].trim();
27089
+ const tokens = argStr.split(/\s+/);
27090
+ const varNames = [];
27091
+ for (let ti = 0; ti < tokens.length; ti++) {
27092
+ const tok = tokens[ti];
27093
+ if (!tok) continue;
27094
+ if (tok.startsWith("-")) {
27095
+ if (/^-[cCnOsud]$/.test(tok)) ti++;
27096
+ continue;
27097
+ }
27098
+ if (/^[A-Za-z_][\w]*$/.test(tok)) varNames.push(tok);
27099
+ }
27100
+ if (varNames.length === 0) varNames.push("MAPFILE");
27101
+ for (const v of varNames) {
27102
+ const already = sources.some((s) => s.line === lineNumber && s.variable === v);
27103
+ if (already) continue;
27104
+ sources.push({
27105
+ type: "io_input",
27106
+ location: `mapfile \u2192 $${v} (stdin array)`,
27107
+ severity: "high",
27108
+ line: lineNumber,
27109
+ confidence: 0.9,
27110
+ variable: v
27111
+ });
27112
+ }
27113
+ }
27114
+ const getoptsMatch = trimmed.match(/\bgetopts\s+["'][^"']+["']\s+(\w+)/);
27115
+ if (getoptsMatch) {
27116
+ const flagVar = getoptsMatch[1];
27117
+ for (const v of [flagVar, "OPTARG"]) {
27118
+ const already = sources.some((s) => s.line === lineNumber && s.variable === v);
27119
+ if (already) continue;
27120
+ sources.push({
27121
+ type: "io_input",
27122
+ location: `getopts \u2192 $${v} (CLI arg)`,
27123
+ severity: "high",
27124
+ line: lineNumber,
27125
+ confidence: 0.9,
27126
+ variable: v
27127
+ });
27128
+ }
27129
+ }
26820
27130
  const envRe = /\$([A-Z][A-Z0-9_]{2,})|\$\{([A-Z][A-Z0-9_]{2,})\}/g;
26821
27131
  let em;
26822
27132
  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.
@@ -12749,6 +12792,91 @@ var DEFAULT_SANITIZERS = [
12749
12792
  // Command injection - shell escaping
12750
12793
  { method: "quote", class: "shell", removes: ["command_injection"] },
12751
12794
  { method: "escape", class: "shell-escape", removes: ["command_injection"] },
12795
+ // JS/Node — additional XSS/HTML encoders (cognium-dev #213 sixth slice).
12796
+ //
12797
+ // he.encode(x) / he.escape(x) — `he` npm package
12798
+ // sanitizeHtml(x) — `sanitize-html` npm
12799
+ // xss(x) / filterXSS(x) — `xss` npm (Yahoo)
12800
+ // escapeHtml(x) / escapeHTML(x) — common bare helpers
12801
+ // entities.encode(x) / entities.escape(x) — `entities` npm
12802
+ // xssFilters.inHTMLData(x) / inHTMLComment(x) / uriInHTMLData(x)
12803
+ // — `xss-filters` npm (Yahoo)
12804
+ //
12805
+ // `external_taint_escape` is included alongside `xss` — the CWE-668
12806
+ // fallback fires on any call receiving tainted data that the engine
12807
+ // does not otherwise recognize. Since these functions are explicitly
12808
+ // registered as sanitizers (they PURIFY input), they should also
12809
+ // suppress the fallback so a `res.send(sanitizeHtml(x))` flow does
12810
+ // not report an external-taint-escape at the sanitizer call itself.
12811
+ { method: "encode", class: "he", removes: ["xss", "external_taint_escape"] },
12812
+ { method: "escape", class: "he", removes: ["xss", "external_taint_escape"] },
12813
+ { method: "sanitizeHtml", removes: ["xss", "external_taint_escape"] },
12814
+ { method: "xss", removes: ["xss", "external_taint_escape"] },
12815
+ { method: "filterXSS", removes: ["xss", "external_taint_escape"] },
12816
+ { method: "escapeHtml", removes: ["xss", "external_taint_escape"] },
12817
+ { method: "escapeHTML", removes: ["xss", "external_taint_escape"] },
12818
+ { method: "encode", class: "entities", removes: ["xss", "external_taint_escape"] },
12819
+ { method: "escape", class: "entities", removes: ["xss", "external_taint_escape"] },
12820
+ // xss-filters: context-specific helpers, all XSS-safe by construction.
12821
+ { method: "inHTMLData", class: "xssFilters", removes: ["xss", "external_taint_escape"] },
12822
+ { method: "inHTMLComment", class: "xssFilters", removes: ["xss", "external_taint_escape"] },
12823
+ { method: "uriInHTMLData", class: "xssFilters", removes: ["xss", "external_taint_escape"] },
12824
+ // JS/Node — SQL escaping (in addition to the mysql class above).
12825
+ //
12826
+ // SqlString.escape / .escapeId — `sqlstring` npm (raw
12827
+ // escape used by mysql /
12828
+ // node-mysql2 drivers)
12829
+ // pgFormat(sql, ...args) / format(bare) — `pg-format` npm
12830
+ // SQL.raw / SQL(bare) — `sql-template-strings`
12831
+ //
12832
+ // Bare `format` is intentionally NOT registered — it collides with
12833
+ // string.format-style helpers unrelated to SQL. Callers should use the
12834
+ // qualified form.
12835
+ { method: "escape", class: "SqlString", removes: ["sql_injection", "external_taint_escape"] },
12836
+ { method: "escapeId", class: "SqlString", removes: ["sql_injection", "external_taint_escape"] },
12837
+ { method: "format", class: "SqlString", removes: ["sql_injection", "external_taint_escape"] },
12838
+ { method: "format", class: "pgFormat", removes: ["sql_injection", "external_taint_escape"] },
12839
+ { method: "ident", class: "pgFormat", removes: ["sql_injection", "external_taint_escape"] },
12840
+ { method: "literal", class: "pgFormat", removes: ["sql_injection", "external_taint_escape"] },
12841
+ // JS/Node — crypto.randomUUID() and Node's crypto.randomBytes(...)toString()
12842
+ // produce cryptographically-secure typed strings that cannot carry
12843
+ // attacker-injected payload. Registered as type coercion.
12844
+ { method: "randomUUID", class: "crypto", removes: ["sql_injection", "nosql_injection", "command_injection", "path_traversal", "code_injection", "xss"] },
12845
+ // Bare `randomUUID()` alias — `import { randomUUID } from 'crypto'`.
12846
+ { method: "randomUUID", removes: ["sql_injection", "nosql_injection", "command_injection", "path_traversal", "code_injection", "xss"] },
12847
+ // JS/Node — URL construction. `new URL(x, base)` (constructor-call
12848
+ // matched by method_name === 'URL') and `URLSearchParams.toString()`
12849
+ // safely encode components for URL context.
12850
+ { method: "toString", class: "URLSearchParams", removes: ["ssrf", "open_redirect", "xss"] },
12851
+ // Go — additional sanitizers (cognium-dev #213 sixth slice).
12852
+ //
12853
+ // regexp.QuoteMeta(x) — mirror of Python re.escape; strips regex
12854
+ // metacharacters so downstream compile/match
12855
+ // cannot execute attacker-crafted patterns.
12856
+ // bluemonday.Sanitize — dominant Go HTML sanitizer library
12857
+ // (`microcosm-cc/bluemonday`). Called on a
12858
+ // Policy value returned by UGCPolicy() /
12859
+ // StrictPolicy() / etc. Match by method name
12860
+ // alone — Policy is the receiver type.
12861
+ // net.ParseIP(x) — validates the input is a well-formed IP;
12862
+ // returns nil for bad input (developer must
12863
+ // check, but the sanitizer only applies when
12864
+ // the parsed IP flows onward).
12865
+ // sql.Named(name, val) — parameterized query binding.
12866
+ { method: "QuoteMeta", class: "regexp", removes: ["redos", "code_injection", "external_taint_escape"] },
12867
+ { method: "Sanitize", class: "bluemonday", removes: ["xss", "external_taint_escape"] },
12868
+ { method: "Sanitize", class: "Policy", removes: ["xss", "external_taint_escape"] },
12869
+ // Bare `Sanitize` — the receiver is a Policy variable (`p := bluemonday.UGCPolicy(); p.Sanitize(x)`).
12870
+ // `Policy` class-matching only fires when the IR resolves the receiver type;
12871
+ // since Go DFG rarely propagates the type of a locally-assigned variable
12872
+ // from a package factory call, we also register the bare method name.
12873
+ // Narrow FP risk: `Sanitize` bare is uncommon outside bluemonday context.
12874
+ { method: "Sanitize", removes: ["xss", "external_taint_escape"] },
12875
+ { method: "SanitizeBytes", class: "bluemonday", removes: ["xss", "external_taint_escape"] },
12876
+ { method: "SanitizeBytes", class: "Policy", removes: ["xss", "external_taint_escape"] },
12877
+ { method: "SanitizeBytes", removes: ["xss", "external_taint_escape"] },
12878
+ { method: "ParseIP", class: "net", removes: ["ssrf", "command_injection", "path_traversal", "external_taint_escape"] },
12879
+ { method: "Named", class: "sql", removes: ["sql_injection", "external_taint_escape"] },
12752
12880
  // =========================================================================
12753
12881
  // Python Sanitizers
12754
12882
  // =========================================================================
@@ -12793,6 +12921,60 @@ var DEFAULT_SANITIZERS = [
12793
12921
  // Python Type coercion
12794
12922
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
12795
12923
  { method: "float", removes: ["sql_injection", "command_injection"] },
12924
+ // Python URL encoding (cognium-dev #213 fifth slice).
12925
+ //
12926
+ // urllib.parse.quote(x) — RFC-3986 percent-encode; safe for URL path
12927
+ // urllib.parse.quote_plus(x) — same + space→+; safe for query string
12928
+ // urllib.parse.urlencode(d) — encode a dict of pairs
12929
+ //
12930
+ // Bounded to URL-context sinks. Class-scoped to the specific
12931
+ // `urllib.parse` receiver to avoid colliding with unrelated bare
12932
+ // `quote(...)` calls in other libraries.
12933
+ { method: "quote", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
12934
+ { method: "quote_plus", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
12935
+ { method: "urlencode", class: "urllib.parse", removes: ["ssrf", "open_redirect", "xss"] },
12936
+ // Bare aliases — `from urllib.parse import quote` then unqualified.
12937
+ { method: "quote_plus", removes: ["ssrf", "open_redirect", "xss", "path_traversal"] },
12938
+ { method: "urlencode", removes: ["ssrf", "open_redirect", "xss"] },
12939
+ // `quote` bare is intentionally NOT registered — it collides with
12940
+ // shlex.quote (bare-imported) which is a command_injection sanitizer,
12941
+ // not a URL sanitizer. The class-scoped variant above catches the
12942
+ // qualified `urllib.parse.quote(...)` shape; unqualified callers who
12943
+ // want URL-context credit should use `quote_plus` (which does not
12944
+ // collide with any command_injection sanitizer).
12945
+ // Python XSS — additional common sanitizers.
12946
+ //
12947
+ // bleach.clean(...) — already covered above (line 2898)
12948
+ // bleach.linkify(...) — turns URLs into anchor tags; sanitizes as well
12949
+ // django.utils.html.escape / strip_tags — Django's XSS escape helpers
12950
+ // jinja2.escape — Jinja2's Markup escape (aliased from markupsafe)
12951
+ // flask.escape — Flask re-export of markupsafe.escape
12952
+ // saxutils.escape — stdlib xml.sax.saxutils.escape for XML docs
12953
+ { method: "linkify", class: "bleach", removes: ["xss"] },
12954
+ // Bare `linkify(...)` alias — `from bleach import linkify`.
12955
+ { method: "linkify", removes: ["xss"] },
12956
+ { method: "escape", class: "django.utils.html", removes: ["xss"] },
12957
+ { method: "strip_tags", class: "django.utils.html", removes: ["xss"] },
12958
+ { method: "escape", class: "jinja2", removes: ["xss"] },
12959
+ { method: "escape", class: "flask", removes: ["xss"] },
12960
+ { method: "escape", class: "saxutils", removes: ["xss"] },
12961
+ { method: "escape", class: "xml.sax.saxutils", removes: ["xss"] },
12962
+ { method: "quoteattr", class: "saxutils", removes: ["xss"] },
12963
+ { method: "quoteattr", class: "xml.sax.saxutils", removes: ["xss"] },
12964
+ // Python ReDoS — `re.escape(user)` when building a regex from user input
12965
+ // strips regex metacharacters. Downstream `re.compile / re.match` cannot
12966
+ // interpret user-supplied alternations or quantifiers. Also covers the
12967
+ // `code_injection` categorization that `re.compile` currently emits
12968
+ // (a re.escape-wrapped pattern cannot execute anything, so both are safe).
12969
+ { method: "escape", class: "re", removes: ["redos", "code_injection"] },
12970
+ // Python SQLAlchemy — `text(...).bindparams(...)` binds params safely.
12971
+ // The `bindparams` call is the sanitizer; the parent `text` wraps the
12972
+ // template. Also add `expression.literal` for explicit SQL literals.
12973
+ { method: "bindparams", removes: ["sql_injection"] },
12974
+ // psycopg2 sql-composition helpers
12975
+ { method: "Identifier", class: "sql", removes: ["sql_injection"] },
12976
+ { method: "Literal", class: "sql", removes: ["sql_injection"] },
12977
+ { method: "Placeholder", class: "sql", removes: ["sql_injection"] },
12796
12978
  // =========================================================================
12797
12979
  // Rust Sanitizers
12798
12980
  // =========================================================================
@@ -14621,7 +14803,14 @@ function matchesSanitizerPattern(call, pattern) {
14621
14803
  return false;
14622
14804
  }
14623
14805
  if (pattern.class) {
14624
- if (!call.receiver || !receiverMightBeClass(call.receiver, pattern.class)) {
14806
+ if (!call.receiver) {
14807
+ const target = call.resolution?.target;
14808
+ const expectedTail = `${pattern.class}.${pattern.method}`;
14809
+ if (target && (target === expectedTail || target.endsWith("." + expectedTail))) {
14810
+ } else {
14811
+ return false;
14812
+ }
14813
+ } else if (!receiverMightBeClass(call.receiver, pattern.class)) {
14625
14814
  return false;
14626
14815
  }
14627
14816
  }