# Patch Analysis: CVE-2026-8054 / GHSA-jpx3-25r2-jq5g

## Fix Overview

The vendor fix (dotCMS/core PR #35553, merged as `6a5f4188715baaf5b4ffdf0f8f80c402ccfb97ab`) closes the unauthenticated SQL injection in the Publish Audit API by making two complementary changes:

1. **Parameterizes the bundle-id query** in `com.dotcms.publisher.business.PublishAuditAPIImpl.getPublishAuditStatuses(List<String>)`:
   - Replaces the string concatenation `bundleIds.stream().map(id -> "'" + id + "'").collect(...)` with `?` placeholders and `dc.addParam()` for each bundle ID.
   - Eliminates the SQL injection sink completely for the patched method.

2. **Adds push-publish authentication** to `com.dotcms.rest.AuditPublishingResource`:
   - Both `@GET /get/{bundleId}` and `@POST /getAll` now call `AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request)` and reject anonymous requests through `PushPublishResourceUtil.getFailResponse()`.
   - The authentication can be satisfied by a valid JWT (admin-only) or by an endpoint auth key tied to the remote IP of a configured push-publish endpoint.

3. **Supporting changes**:
   - `PublisherQueueJob` adds an `Authorization` header when it internally calls `/api/auditPublishing/getAll` so the now-authenticated endpoint remains usable for legitimate server-to-server push-publish traffic.
   - `AuthCredentialPushPublishUtil.getTokenFromRequest()` was relaxed to return `StringUtils.EMPTY` instead of throwing an `IllegalArgumentException` when the header is missing or non-Bearer, avoiding accidental 500 responses.

## Fix Assumptions

- The only external entry point to `getPublishAuditStatuses(List<String>)` is the `AuditPublishingResource.getAll` REST endpoint; once that endpoint is authenticated and the method is parameterized, no unauthenticated path can reach the sink.
- The `PushPublishResourceUtil` authentication logic is robust enough to reject all anonymous requests and requests with spoofed/invalid push-publish credentials.
- The single-ID query `getPublishAuditStatus(String)` has always been parameterized, so no corresponding SQL injection existed there.
- LTS releases are not part of the immediate patch scope; the ticket notes that the fix was **not backported** to LTS.

## What the Fix Does NOT Cover

- **Other SQL-concatenation patterns elsewhere in the codebase** (e.g., `BrowserAPIImpl` historically built `IN (...)` clauses from content-type IDs). Those are not part of the Publish Audit trust boundary and are not claimed by this CVE, so they are out-of-scope for this variant analysis.
- **Information disclosure through the GET endpoint before authentication**: In the vulnerable version, `GET /api/auditPublishing/get/{bundleId}` is also reachable without authentication, but it uses a parameterized query and therefore does not constitute a SQL injection. The fix does protect it with authentication, but the primary CVE impact is the SQL injection in `POST /getAll`.
- **Pre-2026 LTS branches**: The vulnerable `getPublishAuditStatuses(List<String>)` method was introduced in commit `09fbef6b5a9f02e9f70804251783ff267c85eaf6` (2026-01-22). Older 24.x LTS images (e.g., `24.12.27_lts_v23`) do not contain that method at all, so they are not affected by this specific bug. Any 25.x/26.x LTS branch that does contain the method would need its own backport.

## Gaps Evaluated

| Candidate path | Why it was evaluated | Result on fixed version |
|---|---|---|
| `POST /api/auditPublishing/getAll` with original `pg_sleep` JSON array | Positive control | 401, no timing delay |
| `GET /api/auditPublishing/get/{bundleId}` with SQLi payload | Different endpoint / different data shape | 401, no timing delay |
| `POST /api/auditPublishing/getAll` with a single string body instead of JSON array | Different input type / deserialization bypass | 400 on both versions (JAX-RS rejects) |
| `POST /api/auditPublishing/getAll` with empty JSON array | Edge case that hit `bundleIds.get(0)` NPE in vulnerable code | 401 on fixed version |
| `POST /api/auditPublishing/getAll` with `Content-Type: text/plain` or missing `Content-Type` | Content negotiation bypass | 415 on both versions |
| `POST /api/auditPublishing/getAll` with `Authorization: Bearer ` (empty) | JWT syntax edge case / auth bypass | 401 on fixed version |
| `POST /api/auditPublishing/getAll` with `X-Forwarded-For: 127.0.0.1` | IP-based endpoint-key bypass | 401 on fixed version |
| `POST /api/auditPublishing/getAll` with `X-HTTP-Method-Override: GET` | HTTP method override | 401 on fixed version |

No candidate produced a time-delay (>= 4s) HTTP 200 response on the fixed version, so no bypass or distinct variant of the SQL injection was confirmed.

## Threat-Model Context

dotCMS's `SECURITY.md` states that security fixes are backported to LTS when possible. The Publish Audit API is intended for push-publish server-to-server communication. The fix aligns the authentication model with that expectation and removes the unauthenticated SQL injection primitive. The tested variant candidates do not cross a trust boundary that the vendor claims to protect, and the fixed code covers the only external entry point to the vulnerable sink.

## Conclusion

The patch is **complete for the claimed surface**: the SQL injection sink is parameterized and the only unauthenticated external entry point is authenticated. The variant search did not find a bypass or a materially different entry point that still reaches the same vulnerable sink.

