# Variant RCA: CVE-2026-8054 / GHSA-jpx3-25r2-jq5g

## Summary

CVE-2026-8054 is an unauthenticated, time-based SQL injection in the dotCMS Core Publish Audit API. The original reproduction confirmed that `POST /api/auditPublishing/getAll` with a `pg_sleep` payload in a JSON array element delays ~5 seconds on the vulnerable build (`26.04.28-02`) and is rejected with HTTP 401 on the fixed build (`26.04.28-03`). This variant stage exhaustively tested the same and adjacent endpoints, different request bodies/content types, and several authentication-bypass tricks against the fixed build. No distinct bypass or alternate entry point that reaches the same SQL-injection sink was confirmed. The GET endpoint (`/api/auditPublishing/get/{bundleId}`) is reachable unauthenticated in the vulnerable version but uses a parameterized query, so it does not trigger the same SQLi root cause. LTS 24.x images do not contain the vulnerable `getPublishAuditStatuses(List<String>)` method at all.

## Fix Coverage / Assumptions

The vendor patch (PR #35553, commit `6a5f4188715baaf5b4ffdf0f8f80c402ccfb97ab`) relies on two invariants:

1. **Sink parameterization**: `com.dotcms.publisher.business.PublishAuditAPIImpl.getPublishAuditStatuses(List<String>)` now builds `IN (...)` with `?` placeholders and calls `dc.addParam()` for each bundle ID, so no attacker-controlled string can be concatenated into the SQL text.
2. **Endpoint authentication**: `com.dotcms.rest.AuditPublishingResource.get()` and `.getAll()` now require a valid push-publish token (JWT admin token or endpoint auth key tied to the remote IP). Anonymous requests are rejected by `PushPublishResourceUtil.getFailResponse()`.

The patch explicitly covers the only external entry point to the vulnerable sink (`AuditPublishingResource.getAll`). `PublisherQueueJob` was updated to send the required `Authorization` header for legitimate internal use.

## Variant / Alternate Trigger

The following candidate paths were tested against both the vulnerable and fixed builds via `bundle/vuln_variant/reproduction_steps.sh`:

1. **Positive control** — `POST /api/auditPublishing/getAll` with the original `pg_sleep` JSON array payload.
2. **GET path variant** — `GET /api/auditPublishing/get/{bundleId}` where the path parameter contains a `pg_sleep` payload.
3. **Non-array body variant** — `POST /api/auditPublishing/getAll` with a single JSON string body instead of a list.
4. **Empty list edge case** — `POST /api/auditPublishing/getAll` with `[]`.
5. **Content-type variants** — `text/plain` body and missing `Content-Type`.
6. **Auth bypass: empty Bearer** — `Authorization: Bearer `.
7. **Auth bypass: IP spoofing header** — `X-Forwarded-For: 127.0.0.1`.
8. **Auth bypass: HTTP method override** — `X-HTTP-Method-Override: GET`.

None of the candidates produced a `duration >= 4.0s` HTTP 200 response on the fixed build. The only code path that still reaches the original sink is the internal `PublisherQueueJob`, which now supplies a valid push-publish token and is not attacker-reachable.

## Impact

- **Product / component:** dotCMS Core Publish Audit API (`com.dotcms.rest.AuditPublishingResource`, `com.dotcms.publisher.business.PublishAuditAPIImpl`).
- **Affected versions tested:** `26.04.28-02` (vulnerable), `26.04.28-03` (fixed).
- **Risk:** Critical for the original unauthenticated SQLi; no additional unauthenticated SQLi path was reproduced in this stage.
- **Consequences:** No new data breach or database-compromise primitive was found beyond the already-disclosed POST `/getAll` path.

## Impact Parity

- **Disclosed / claimed maximum impact:** Unauthenticated remote SQL injection allowing arbitrary database read/modify/destroy.
- **Reproduced impact from this variant stage:** Only the original POST `/getAll` time-based SQLi reproduced on the vulnerable build; the fixed build rejected all tested payloads with HTTP 401 and no time delay.
- **Parity:** `none` for a new variant — no additional SQLi surface or bypass was demonstrated.
- **Not demonstrated:** A concrete read, modify, or destroy query on any new path; a bypass of the authentication on the fixed build; a variant that works against the LTS line.

## Root Cause

The same SQL-injection root cause (string concatenation of bundle IDs into an `IN (...)` clause) can only be reached through `PublishAuditAPIImpl.getPublishAuditStatuses(List<String>)`. The patch removes that concatenation entirely. The only REST endpoint that historically called that method, `AuditPublishingResource.getAll`, is now authenticated. Because the single-ID method `getPublishAuditStatus(String)` has always been parameterized, the GET endpoint never shared the same SQLi sink.

## Reproduction Steps

1. Run `bash bundle/vuln_variant/reproduction_steps.sh`.
2. The script starts a vulnerable dotCMS stack (`26.04.28-02`) and a fixed dotCMS stack (`26.04.28-03`), each with Postgres and OpenSearch.
3. It waits for both stacks to be live, then sends the eight candidate requests to each stack from a Python sidecar.
4. It compares HTTP status codes and response durations and writes `bundle/logs/vuln_variant_analysis.json`.
5. Expected evidence: vulnerable build returns HTTP 200 with ~5s delay for the original POST `/getAll` payload; fixed build returns HTTP 401 with no delay for every candidate.

## Evidence

- Variant script: `bundle/vuln_variant/reproduction_steps.sh`
- Per-stack results: `bundle/logs/vuln_variant_results.json`, `bundle/logs/fixed_variant_results.json`
- Analysis summary: `bundle/logs/vuln_variant_analysis.json`
- Runtime log: `bundle/logs/vuln_variant_reproduction_steps.log`
- Patch source: `https://github.com/dotCMS/core/pull/35553` (merge commit `6a5f4188715baaf5b4ffdf0f8f80c402ccfb97ab`)

Key excerpts from the second run:

| Test | Vuln status/duration | Fixed status/duration |
|---|---|---|
| `getAll_true_condition` (positive control) | 200 / 5.02s | 401 / 0.007s |
| `get_path_sqli` | 404 / 0.047s | 401 / 0.010s |
| `getAll_empty_bearer` | 200 / 5.02s | 401 / 0.006s |
| `getAll_x_forwarded_for_localhost` | 200 / 5.02s | 401 / 0.006s |
| `getAll_method_override` | 200 / 5.02s | 401 / 0.006s |

## Recommendations / Next Steps

- **No additional code change is required** for the specific CVE-2026-8054 surface: the patch fully covers the SQLi sink and the external entry point.
- **Hardening idea**: Consider adding authentication enforcement at the JAX-RS class level or a servlet filter for all `/auditPublishing` paths, so any future methods added to `AuditPublishingResource` inherit the same protection.
- **LTS policy**: If any 25.x/26.x LTS branch contains the method introduced by `09fbef6b5a9f02e9f70804251783ff267c85eaf6`, the same two-part fix (parameterization + authentication) should be backported.
- **Code audit**: The same `id -> "'" + id + "'"` concatenation pattern found in `BrowserAPIImpl` (out of scope for this CVE) should be reviewed for authenticated SQLi risk independently.

## Additional Notes

- The script was run twice and produced consistent results; both runs exited 1 because no bypass/variant was found, which is the expected outcome for a negative variant search.
- The vulnerable build returns HTTP 500 for `POST /api/auditPublishing/getAll` with `[]` because the code calls `bundleIds.get(0)` in the exception handler. This is a secondary bug but not a SQL-injection variant.
- LTS 24.x images (e.g., `24.12.27_lts_v23`) were not tested by runtime because bytecode inspection showed they do not contain `getPublishAuditStatuses(List<String>)`, so the vulnerable sink is absent.

