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.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +182 -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 +312 -2
- package/dist/core/circle-ir-core.cjs +190 -1
- package/dist/core/circle-ir-core.js +190 -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.
|
|
@@ -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
|
|
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
|
|
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
|
}
|