# Root Cause Analysis: CVE-2026-8054

## Summary

CVE-2026-8054 is an unauthenticated, time-based SQL injection in the dotCMS Core Publish Audit API. The affected endpoints (`/api/auditPublishing/get` and `/api/auditPublishing/getAll`) accept attacker-controlled bundle identifiers that are concatenated into an `IN (...)` SQL clause without parameterization. A remote unauthenticated attacker can inject PostgreSQL `pg_sleep` payloads and observe response delays of several seconds, demonstrating that attacker input is executed as SQL in the backend database. The issue is fixed in dotCMS Core 26.04.28-03, which both parameterizes the bundle-id query and enforces push-publish authentication on the resource.

## Impact

- **Product / component:** dotCMS Core Publish Audit API (`com.dotcms.rest.AuditPublishingResource`, `com.dotcms.publisher.business.PublishAuditAPIImpl`)
- **Affected versions:** 25.11.04-1 through 26.04.28-02 (per advisory data; not backported to LTS)
- **Fixed version:** 26.04.28-03
- **Risk:** Critical. An unauthenticated attacker can read, modify, or destroy database contents, because the SQL injection runs with the privileges of the dotCMS application user. The demonstrated time-based blind payload proves that the database evaluates attacker-controlled SQL.
- **Consequences:** Data breach, data integrity loss, and potential full database compromise.

## Impact Parity

- **Disclosed / claimed maximum impact:** SQL injection in an unauthenticated remote API, allowing arbitrary database content read/modify/destroy.
- **Reproduced impact:** Unauthenticated, time-based blind SQL injection through `POST /api/auditPublishing/getAll` with a `pg_sleep(5)` payload. The vulnerable build returns HTTP 200 after a ~5-second delay, while the fixed build returns HTTP 401 with no delay.
- **Parity:** `partial` for the full read/modify/destroy claim — the reproduction proves SQL injection (a database primitive), but it does not execute a concrete read, modify, or destroy operation. The surface parity is `full` because the same unauthenticated HTTP API path is exercised on both the vulnerable and fixed versions.
- **Not demonstrated:** Direct extraction of data, DML/DDL abuse, or code execution beyond the database boundary.

## Root Cause

The vulnerable code is in `PublishAuditAPIImpl.getPublishAuditStatuses(List<String> bundleIds)`:

```java
final List<String> parameter = bundleIds.stream()
    .map(id -> "'" + id + "'")
    .collect(Collectors.toList());
dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS, String.join(",", parameter)));
```

Each bundle id is wrapped in single quotes and joined directly into the SQL string, so a value such as

```
x' || (SELECT CASE WHEN 1=1 THEN pg_sleep(5)::text ELSE '' END) || '
```

breaks out of the quoted literal and appends a PostgreSQL sleep expression to the generated query. The query is then executed by `DotConnect.loadObjectResults()`, causing the HTTP response to be delayed for the sleep duration.

Additionally, `AuditPublishingResource` had no authentication check on its endpoints, so the request could be sent anonymously. The fix in `dotCMS/core#35553` (commit `dc515d9` and subsequent review commits) addresses both aspects:

1. **Parameterization** — replaces the quote-concatenated `IN` list with `?` placeholders and binds the bundle ids with `dc.addParam(...)`.
2. **Authentication** — adds `processAuthHeader(request)` and `PushPublishResourceUtil.getFailResponse(...)` to `AuditPublishingResource.get(...)` and `getAll(...)`, so unauthenticated callers receive HTTP 401 before the query logic is reached.

## Reproduction Steps

The reproduction is fully automated by `bundle/repro/reproduction_steps.sh`:

1. Ensures the required Docker images are available (`dotcms/dotcms:26.04.28-02`, `dotcms/dotcms:26.04.28-03`, `postgres:15`, `opensearchproject/opensearch:1.3.19`).
2. Spawns a per-version stack: one OpenSearch container, one PostgreSQL container, and one dotCMS container on a dedicated Docker network.
3. Waits for dotCMS to finish startup (`MainServlet init completed` in the container log and HTTP 200 from `/api/v1/system/status`).
4. Sends the following unauthenticated HTTP requests to each stack:
   - Baseline: `POST /api/auditPublishing/getAll` with body `["x' || (SELECT CASE WHEN 1=2 THEN pg_sleep(0)::text ELSE '' END) || '"]` (no sleep, should be fast).
   - Time-delay trigger: `POST /api/auditPublishing/getAll` with body `["x' || (SELECT CASE WHEN 1=1 THEN pg_sleep(5)::text ELSE '' END) || '"]` (should delay ~5 seconds).
   - Optional GET probe: `GET /api/auditPublishing/get?bundleId=<url-encoded sqli>`.
5. Compares response times and status codes between the vulnerable and fixed builds.

### Expected evidence

- Vulnerable build (26.04.28-02): `POST /api/auditPublishing/getAll` with the true payload returns HTTP 200 and takes at least ~5 seconds.
- Fixed build (26.04.28-03): the same request returns HTTP 401 and takes well under 1 second, proving both parameterization and authentication gating.

## Evidence

- Runtime manifest: `bundle/repro/runtime_manifest.json`
- Reproduction log: `bundle/logs/reproduction_steps.log`
- Per-stack results: `bundle/logs/vuln_results.json`, `bundle/logs/fixed_results.json`
- Timing summary: `bundle/logs/timing_summary.tsv`
- Container logs: `bundle/logs/vuln_dotcms_container.log`, `bundle/logs/fixed_dotcms_container.log`, `bundle/logs/vuln_postgres_container.log`, `bundle/logs/fixed_postgres_container.log`, `bundle/logs/vuln_opensearch_container.log`, `bundle/logs/fixed_opensearch_container.log`
- Nuclei template reference: `https://raw.githubusercontent.com/projectdiscovery/nuclei-templates/main/http/cves/2026/CVE-2026-8054.yaml`
- Fix commit / PR: `https://github.com/dotCMS/core/pull/35553`

Key excerpts from the current run (see `bundle/logs/timing_summary.tsv` and `bundle/logs/*_results.json`):

- Vulnerable dotCMS 26.04.28-02 (`POST /api/auditPublishing/getAll` with a false-condition `pg_sleep(0)` payload): HTTP 200, body `[]`, duration 0.061 s.
- Vulnerable dotCMS 26.04.28-02 (`POST /api/auditPublishing/getAll` with a true-condition `pg_sleep(5)` payload): HTTP 200, body `[]`, duration 5.023 s.
- Fixed dotCMS 26.04.28-03 (`POST /api/auditPublishing/getAll` with the same `pg_sleep(5)` payload): HTTP 401, body reports an invalid authentication token, duration 0.007 s.

The ~5-second delay on the vulnerable build and the sub-10-millisecond 401 response on the fixed build demonstrate that the unauthenticated time-based SQL injection is present in 26.04.28-02 and absent in 26.04.28-03.

## Recommendations / Next Steps

1. **Upgrade** to dotCMS Core 26.04.28-03 or later (the patch is not backported to LTS, so LTS users must verify their release line).
2. **Do not roll back** the authentication gating on `AuditPublishingResource`; the endpoints are intended only for push-publish-authenticated callers.
3. **Regression test** with the exact nuclei-template payload and with malicious bundle ids such as `x' OR '1'='1`, `x'; DROP TABLE publishing_queue_audit; --`, and `x' UNION SELECT ...` to confirm parameter binding prevents injection and preserves the `publishing_queue_audit` table.
4. **Review** other REST resources that consume list parameters and build SQL `IN` clauses; ensure they use parameterized queries.

## Additional Notes

- The script is idempotent: it removes and recreates containers, networks, and log files on each run, so it can be executed twice in a row without manual cleanup.
- The reproduction does not rely on ASAN/UBSAN or any static-analysis tool; it exercises the real dotCMS HTTP API through the full stack (PostgreSQL + OpenSearch + dotCMS).
- The GET `/api/auditPublishing/get` endpoint is also protected by the same fix, but the primary time-delay evidence is obtained from `POST /api/auditPublishing/getAll`, which matches the public nuclei template and the advisory description.
