CVE-2026-8054: Verified Repro With Script Download
CVE-2026-8054: Unauthenticated SQL injection in dotCMS Publish Audit API
CVE-2026-8054 is verified against dotCMS/core · github. Affected versions: dotCMS Core 25.11.04-1 through 26.04.28-02. Vulnerability class: SQLi. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00194.
What Is CVE-2026-8054?
CVE-2026-8054 is a critical unauthenticated SQL injection vulnerability in dotCMS Core's Publish Audit API endpoints. Pruva reproduced it (reproduction REPRO-2026-00194).
CVE-2026-8054 Severity & CVSS Score
CVE-2026-8054 is rated critical severity, with a CVSS base score of 10.0 out of 10.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected dotCMS/core Versions
dotCMS/core · github versions dotCMS Core 25.11.04-1 through 26.04.28-02 are affected.
How to Reproduce CVE-2026-8054
pruva-verify REPRO-2026-00194 curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00194/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for CVE-2026-8054
- reached the target end-to-end
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
JSON array element in POST /api/auditPublishing/getAll
- POST /api/auditPublishing/getAll
reproduction_steps.sh No distinct bypass or alternate entry point confirmed for CVE-2026-8054. Exhaustive testing of the fixed dotCMS 26.04.28-03 build shows the patch parameterizes the SQL sink and authenticates all external paths to the Publish Audit API.
How the agent worked
Root Cause and Exploit Chain for CVE-2026-8054
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.
- 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/getAllwith apg_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:
partialfor 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 isfullbecause 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):
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:
- Parameterization — replaces the quote-concatenated
INlist with?placeholders and binds the bundle ids withdc.addParam(...). - Authentication — adds
processAuthHeader(request)andPushPublishResourceUtil.getFailResponse(...)toAuditPublishingResource.get(...)andgetAll(...), 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:
- 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). - Spawns a per-version stack: one OpenSearch container, one PostgreSQL container, and one dotCMS container on a dedicated Docker network.
- Waits for dotCMS to finish startup (
MainServlet init completedin the container log and HTTP 200 from/api/v1/system/status). - Sends the following unauthenticated HTTP requests to each stack:
- Baseline:
POST /api/auditPublishing/getAllwith 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/getAllwith 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>.
- Baseline:
- Compares response times and status codes between the vulnerable and fixed builds.
Expected evidence
- Vulnerable build (26.04.28-02):
POST /api/auditPublishing/getAllwith 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/getAllwith a false-conditionpg_sleep(0)payload): HTTP 200, body[], duration 0.061 s. - Vulnerable dotCMS 26.04.28-02 (
POST /api/auditPublishing/getAllwith a true-conditionpg_sleep(5)payload): HTTP 200, body[], duration 5.023 s. - Fixed dotCMS 26.04.28-03 (
POST /api/auditPublishing/getAllwith the samepg_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
- 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).
- Do not roll back the authentication gating on
AuditPublishingResource; the endpoints are intended only for push-publish-authenticated callers. - 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; --, andx' UNION SELECT ...to confirm parameter binding prevents injection and preserves thepublishing_queue_audittable. - Review other REST resources that consume list parameters and build SQL
INclauses; 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/getendpoint is also protected by the same fix, but the primary time-delay evidence is obtained fromPOST /api/auditPublishing/getAll, which matches the public nuclei template and the advisory description.
Variant Analysis & Alternative Triggers for CVE-2026-8054
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:
- Sink parameterization:
com.dotcms.publisher.business.PublishAuditAPIImpl.getPublishAuditStatuses(List<String>)now buildsIN (...)with?placeholders and callsdc.addParam()for each bundle ID, so no attacker-controlled string can be concatenated into the SQL text. - 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 byPushPublishResourceUtil.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:
- Positive control —
POST /api/auditPublishing/getAllwith the originalpg_sleepJSON array payload. - GET path variant —
GET /api/auditPublishing/get/{bundleId}where the path parameter contains apg_sleeppayload. - Non-array body variant —
POST /api/auditPublishing/getAllwith a single JSON string body instead of a list. - Empty list edge case —
POST /api/auditPublishing/getAllwith[]. - Content-type variants —
text/plainbody and missingContent-Type. - Auth bypass: empty Bearer —
Authorization: Bearer. - Auth bypass: IP spoofing header —
X-Forwarded-For: 127.0.0.1. - 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.
- 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
/getAllpath.
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
/getAlltime-based SQLi reproduced on the vulnerable build; the fixed build rejected all tested payloads with HTTP 401 and no time delay. - Parity:
nonefor 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
- Run
bash bundle/vuln_variant/reproduction_steps.sh. - 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. - It waits for both stacks to be live, then sends the eight candidate requests to each stack from a Python sidecar.
- It compares HTTP status codes and response durations and writes
bundle/logs/vuln_variant_analysis.json. - Expected evidence: vulnerable build returns HTTP 200 with ~5s delay for the original POST
/getAllpayload; 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 commit6a5f4188715baaf5b4ffdf0f8f80c402ccfb97ab)
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
/auditPublishingpaths, so any future methods added toAuditPublishingResourceinherit 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 inBrowserAPIImpl(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/getAllwith[]because the code callsbundleIds.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 containgetPublishAuditStatuses(List<String>), so the vulnerable sink is absent.
CVE-2026-8054 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
Artifacts and Evidence for CVE-2026-8054
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix CVE-2026-8054
FAQ: CVE-2026-8054
Which dotCMS Core versions are affected by CVE-2026-8054, and where is it fixed?
How severe is CVE-2026-8054?
How can I reproduce CVE-2026-8054?
References for CVE-2026-8054
Authoritative sources for CVE-2026-8054 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.