1. Overview
A critical SQL injection vulnerability exists in the dotCMS Core content management system’s Publish Audit API. The /api/auditPublishing/getAll REST endpoint accepts a JSON array of bundle identifiers and passes them unsanitized into a SQL query via string concatenation, allowing an attacker to inject arbitrary SQL statements. The endpoint requires no authentication, enabling an unauthenticated remote attacker to read, modify, or destroy the entire dotCMS PostgreSQL database with a single HTTP request. dotCMS addressed this vulnerability in version 26.04.28-03 by parameterizing the SQL query and adding Push Publish JWT token authentication to the affected endpoints.
2. Vulnerability Type
| Field | Value |
|---|---|
| Primary CWE | CWE-89: Improper Neutralization of Special Elements used in an SQL Command (‘SQL Injection’) |
| Related CWE | CWE-306: Missing Authentication for Critical Function |
The primary vulnerability is classic SQL injection via string concatenation into a parameterizable query. The missing authentication is a compounding factor that elevates the severity from post-auth to pre-auth.
3. Severity
CVSS 4.0 (from NVD)
| Field | Value |
|---|---|
| Score | 10.0 (Critical) |
| Vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H |
Our Assessment (CVSS 4.0)
| Metric Group | Metric | Value |
|---|---|---|
| Base — Exploitability | Attack Vector (AV) | Network |
| Attack Complexity (AC) | Low | |
| Attack Requirements (AT) | None | |
| Privileges Required (PR) | None | |
| User Interaction (UI) | None | |
| Base — Vulnerable System | Confidentiality (VC) | High |
| Integrity (VI) | High | |
| Availability (VA) | High | |
| Base — Subsequent System | Confidentiality (SC) | High |
| Integrity (SI) | High | |
| Availability (SA) | High | |
| Threat | Exploit Maturity (E) | Proof-of-Concept |
| Field | Value |
|---|---|
| CVSS 4.0 Score | 10.0 (Critical) |
| CVSS 4.0 Vector | CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:P |
Assessment rationale: The vulnerability is trivially exploitable with no authentication, no special configuration, and no user interaction. The attacker obtains full read/write/delete access to the database, which contains all CMS content, user credentials, API keys, and configuration. The Subsequent System impact is High across all three metrics because dotCMS manages content served to end users (integrity compromise of published websites), stores credentials for integrated systems (confidentiality compromise of downstream services), and a database destruction attack would take the entire CMS offline (availability compromise of all dependent services). The default dotCMS database user is the database owner, granting full schema-level control.
4. Affected Products
Affected Products
| Product | Version Range | CPE 2.3 |
|---|---|---|
| dotCMS Core | 25.11.04-1 through 26.04.28-02 | cpe:2.3:a:dotcms:dotcms:*:*:*:*:*:*:*:* |
Note: LTS releases (24.12.27 LTS, 25.07.10 LTS) are not affected.
Tested Environment (Vulnerable)
| Field | Value |
|---|---|
| Product | dotCMS Core |
| Version | 26.04.21-01 |
| Docker Image | dotcms/dotcms:26.04.21-01 |
| Runtime | Apache Tomcat 9.0.113 / OpenJDK |
| Database | PostgreSQL 16.14 (Alpine) |
| Search | OpenSearch 1.x |
| Vulnerable Class | com.dotcms.publisher.business.PublishAuditAPIImpl |
| Vulnerable Endpoint | com.dotcms.rest.AuditPublishingResource |
Tested Environment (Patched)
| Field | Value |
|---|---|
| Product | dotCMS Core |
| Version | 26.04.28-03+ |
| Fix PR | dotCMS/core#35553 |
| Fix Date | 2026-05-06 (merged) |
| Fix Description | Parameterized SQL queries + Push Publish JWT authentication |
5. Root Cause Analysis
5a. Detailed Description
dotCMS is an open-source Java CMS that supports “Push Publishing” — replicating content between dotCMS instances. The publishing_queue_audit table tracks bundle delivery status. Two REST endpoints expose audit queries:
| Endpoint | Method | Purpose |
|---|---|---|
/api/auditPublishing/get/{bundleId} | GET | Single bundle status |
/api/auditPublishing/getAll | POST | Multiple bundle statuses (JSON array body) |
The /getAll endpoint is served by AuditPublishingResource.getAll(), which receives a List<String> of bundle IDs from the JSON request body and passes them directly to PublishAuditAPIImpl.getPublishAuditStatuses().
The getPublishAuditStatuses() method constructs a SQL IN() clause by wrapping each bundle ID in single quotes via Java string concatenation:
final List<String> parameter = bundleIds.stream()
.map(id -> "'" + id + "'") // wraps in quotes — NOT escaped, NOT parameterized
.collect(Collectors.toList());
dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS,
String.join(",", parameter))); // interpolated directly into SQL via String.format
The SQL template is:
SELECT * FROM publishing_queue_audit WHERE bundle_id IN (%s)
An attacker-controlled bundle ID of x'); SELECT pg_sleep(5)-- produces:
SELECT * FROM publishing_queue_audit WHERE bundle_id IN ('x'); SELECT pg_sleep(5)--')
The PostgreSQL JDBC driver supports stacked queries (multiple SQL statements in a single execute() call), so the attacker can execute arbitrary SQL including INSERT, UPDATE, DELETE, DROP, and SELECT against any table.
The second vulnerability is the complete absence of authentication on both endpoints. The AuditPublishingResource class in vulnerable versions contains no @Context HttpServletRequest parameter, no token validation, no WebResource.InitBuilder, and no call to any authentication utility. Any anonymous HTTP request reaches the vulnerable SQL code path.
The combination of CWE-89 (SQL Injection) and CWE-306 (Missing Authentication) yields a pre-auth, network-reachable SQL injection with CVSS 10.0.
5b. Vulnerable Source Code and Call Path
The following source code was extracted from dotCMS Core version 26.04.21-01 (pre-fix). Comments added by the analyst are marked with **.
AuditPublishingResource.java — REST endpoint (no authentication):
@Path("/auditPublishing")
@Tag(name = "Publishing")
public class AuditPublishingResource {
private PublishAuditAPI auditAPI = PublishAuditAPI.getInstance();
// ** NO @Context HttpServletRequest, NO token check, NO auth of any kind
@POST
@Path("/getAll")
@Produces(MediaType.APPLICATION_JSON)
public Response getAll(List<String> bundleIds) { // ** bundleIds from JSON body
try {
final List<PublishAuditStatus> statuses =
auditAPI.getPublishAuditStatuses(bundleIds); // ** passes unsanitized input
if (statuses != null)
return Response.ok(
statuses.stream()
.map(status -> status.getStatusPojo().getSerialized())
).build();
} catch (DotPublisherException e) {
Logger.warn(this, "error trying to get status for bundle "
+ bundleIds.get(0), e);
}
return Response.status(404).build(); // ** SQL errors caught here
}
}
PublishAuditAPIImpl.java — SQL query construction:
// SQL template
private static final String SELECT_ALL_BY_BUNDLES_IDS =
"select * from publishing_queue_audit where bundle_id in (%s) ";
@Override
@CloseDBIfOpened
public List<PublishAuditStatus> getPublishAuditStatuses(List<String> bundleIds)
throws DotPublisherException {
try {
final List<PublishAuditStatus> result = new ArrayList<>();
DotConnect dc = new DotConnect();
final List<String> parameter = bundleIds.stream()
.map(id -> "'" + id + "'") // ** VULN: string concatenation, no escaping
.collect(Collectors.toList());
dc.setSQL(String.format(
SELECT_ALL_BY_BUNDLES_IDS,
String.join(",", parameter))); // ** VULN: user input interpolated into SQL
List<Map<String, Object>> items = dc.loadObjectResults(); // ** executes the query
for (Map<String, Object> item : items) {
result.add(turnIntoPublishAuditStatus(NO_LIMIT_ASSETS, item));
}
return result;
} catch (Exception e) {
Logger.debug(PublisherUtil.class, e.getMessage(), e); // ** DEBUG level only!
throw new DotPublisherException(
"Unable to get list of elements with error:" + e.getMessage(), e);
}
}
Call path:
HTTP POST /api/auditPublishing/getAll
-> JAX-RS deserializes JSON body into List<String>
-> AuditPublishingResource.getAll(bundleIds) [no auth check]
-> PublishAuditAPI.getPublishAuditStatuses(bundleIds)
-> PublishAuditAPIImpl.getPublishAuditStatuses(bundleIds)
-> "'" + id + "'" for each bundleId [string concat]
-> String.format(SQL_TEMPLATE, joined) [interpolation]
-> DotConnect.setSQL(...)
-> DotConnect.loadObjectResults() [SQL executed]
-> PostgreSQL JDBC executeQuery(...)
5c. Fix (Patched Version)
The fix was applied in PR #35553, merged 2026-05-06 and released in version 26.04.28-03. The fix addresses both vulnerabilities:
Fix 1: Parameterized SQL queries
| Vulnerable | Patched |
|---|---|
bundleIds.stream().map(id -> "'" + id + "'") | bundleIds.stream().map(id -> "?") |
String.join(",", parameter) into String.format(SQL, ...) | Collectors.joining(",") into String.format(SQL, ...) |
| (no parameter binding) | bundleIds.forEach(dc::addParam) |
Patched code:
if (bundleIds == null || bundleIds.isEmpty()) { // ** Added null/empty guard
return Collections.emptyList();
}
// ...
final String placeholders = bundleIds.stream()
.map(id -> "?") // ** FIXED: ? placeholder
.collect(Collectors.joining(","));
dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS, placeholders));
bundleIds.forEach(dc::addParam); // ** FIXED: JDBC parameter binding
Fix 2: Authentication added to endpoints
Both get() and getAll() now validate a Push Publish JWT token via AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request). Requests without a valid Authorization: Bearer header receive HTTP 401.
@POST
@Path("/getAll")
@Produces(MediaType.APPLICATION_JSON)
public Response getAll(final List<String> bundleIds,
@Context final HttpServletRequest request) { // ** Added
final AuthCredentialPushPublishUtil.PushPublishAuthenticationToken ppAuthToken =
AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request);
final Optional<Response> failResponse =
PushPublishResourceUtil.getFailResponse(request, ppAuthToken);
if (failResponse.isPresent()) {
return failResponse.get(); // ** Returns 401
}
// ... parameterized query logic follows
}
Fix 3: Logger correction
The catch block logger was corrected from Logger.debug(PublisherUtil.class, ...) (wrong class, debug level) to Logger.error(PublishAuditAPIImpl.class, ...) (correct class, error level), improving visibility of exploitation attempts.
5d. Impact
The confirmed impact is full read/write/delete access to the dotCMS PostgreSQL database without authentication. During testing, we successfully extracted the complete PostgreSQL version string, user accounts with credentials, and the database schema using stacked INSERT ... SELECT queries injected through a single HTTP POST request. The attack requires one HTTP request for injection and one for retrieval, with no special tools, credentials, or prior knowledge of the target.
The theoretical maximum impact extends to remote code execution on the database server. If the dotCMS database user has PostgreSQL superuser privileges (not the default in Docker deployments but possible in manual installations), the COPY TO PROGRAM command enables arbitrary OS command execution. Even without superuser, the attacker can modify all CMS content (defacement, malware injection into served pages), create admin accounts for persistent access, extract API keys and integration credentials stored in the database, and delete the entire database to cause a permanent denial of service.
6. Proof-of-Concept
6a. PoC Code
Download poc_cve_2026_8054.py (enterprise email verification required)
| File | Description |
|---|---|
poc_cve_2026_8054.py | Python 3 PoC — time-based blind, error-based, and stacked query data exfiltration |
6b. Reproduce Instructions
Prerequisites:
- Container runtime: podman or Docker
- Python 3.6+ (for the PoC script)
- Network access to the target dotCMS instance
Environment Setup:
# Create pod with port mappings
podman pod create --name dotcms-vuln -p 8082:8082
# Start PostgreSQL
podman run -d --pod dotcms-vuln --name dotcms-db \
-e POSTGRES_USER=dotcmsdbuser \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=dotcms \
docker.io/library/postgres:16-alpine
# Start OpenSearch (required by dotCMS)
podman run -d --pod dotcms-vuln --name dotcms-opensearch \
-e "discovery.type=single-node" \
-e "OPENSEARCH_JAVA_OPTS=-Xmx512m -Xms512m" \
-e "DISABLE_SECURITY_PLUGIN=true" \
docker.io/opensearchproject/opensearch:1
# Start vulnerable dotCMS (pre-fix version)
podman run -d --pod dotcms-vuln --name dotcms-app \
-e "CMS_JAVA_OPTS=-Xmx1g" \
-e "DB_BASE_URL=jdbc:postgresql://localhost/dotcms" \
-e "DB_USERNAME=dotcmsdbuser" -e "DB_PASSWORD=password" \
-e "DOT_ES_ENDPOINTS=http://localhost:9200" \
-e "DOT_INITIAL_ADMIN_PASSWORD=admin" \
docker.io/dotcms/dotcms:26.04.21-01
Wait 2-3 minutes for first-boot schema creation. Verify readiness:
curl -s -o /dev/null -w "%{http_code}" http://localhost:8082/api/v1/appconfiguration
# Expected: 200
Reproduction Steps:
- Run the PoC vulnerability test:
python3 poc_cve_2026_8054.py http://localhost:8082 --test
Observe the output confirming SQL injection:
- Endpoint accessible without authentication (HTTP 200)
- Time-based blind confirmed: ~3s delay from
pg_sleep(3)vs ~0.01s baseline - Error-based confirmed: differential response (200 vs 404) for normal vs broken SQL
Demonstrate data exfiltration:
python3 poc_cve_2026_8054.py http://localhost:8082 --dump-version
python3 poc_cve_2026_8054.py http://localhost:8082 --dump-users
- Verify extracted data directly in the database:
podman exec dotcms-db psql -U dotcmsdbuser -d dotcms -c \
"SELECT bundle_id FROM publishing_queue_audit WHERE bundle_id LIKE 'SQLI-POC%';"
6c. Test Results
Vulnerability Detection:
| Test | Baseline | Injected | Result |
|---|---|---|---|
| Authentication check | — | POST without credentials | HTTP 200 (no auth required) |
| Time-based blind (pg_sleep(3)) | 0.01s response | 3.01s response | CONFIRMED (+3.00s delta) |
Error-based (broken SQL x') | HTTP 200, 2 bytes | HTTP 404, 0 bytes | CONFIRMED (differential) |
Data Exfiltration:
| Test | Method | Extracted Data |
|---|---|---|
| Database version | Stacked INSERT…SELECT version() | PostgreSQL 16.14 on x86_64-pc-linux-musl |
| User credentials | Stacked INSERT…SELECT FROM user_ | 4 accounts extracted (including default admin with factory credentials) |
| Database schema | Stacked INSERT…SELECT FROM information_schema.tables | 20+ tables: user_, address, company, passwordtracker, permission_reference, tag, etc. |
6d. Patched System Verification
On the patched version (dotcms/dotcms:26.04.22-02 and later), all requests to /api/auditPublishing/getAll without a valid Push Publish JWT token return:
HTTP/1.1 401
WWW-Authenticate: Bearer realm="example",error="invalid_token",
error_key="__invalid_token__",error_description=""
Content-Type: application/json
The SQL injection payload never reaches the database because the authentication check rejects the request before getPublishAuditStatuses() is called. Additionally, even if authentication were bypassed, the parameterized query binds all bundle IDs as JDBC parameters, preventing SQL injection.
7. Detection
Note: The detection rules below are provided as a starting point. Validate and tune them in your own environment before deploying to production.
7a. Network-Based Detection
Signature-Based Detection
The attack is delivered as an HTTP POST request to /api/auditPublishing/getAll with a JSON array body. Legitimate requests contain UUID-formatted bundle identifiers (e.g., "3fa85f64-5717-4562-b3fc-2c963f66afa6"). Attack traffic contains SQL metacharacters in the JSON string values — specifically single quotes ('), semicolons (;), and SQL comment sequences (--).
Suricata Rules
# CVE-2026-8054: dotCMS Publish Audit API - SQL injection (single quote in JSON body)
alert http any any -> any any (msg:"CVE-2026-8054 dotCMS auditPublishing SQLi - single quote in body"; \
flow:to_server,established; \
http.method; content:"POST"; \
http.uri; content:"/api/auditPublishing/getAll"; \
http.request_body; content:"'"; \
reference:cve,2026-8054; \
classtype:web-application-attack; \
sid:2026805401; rev:1;)
# CVE-2026-8054: dotCMS Publish Audit API - SQL injection (pg_sleep time-based blind)
alert http any any -> any any (msg:"CVE-2026-8054 dotCMS auditPublishing SQLi - pg_sleep"; \
flow:to_server,established; \
http.method; content:"POST"; \
http.uri; content:"/api/auditPublishing/"; \
http.request_body; content:"pg_sleep"; nocase; \
reference:cve,2026-8054; \
classtype:web-application-attack; \
sid:2026805402; rev:1;)
# CVE-2026-8054: dotCMS Publish Audit API - SQL injection (stacked query keywords)
alert http any any -> any any (msg:"CVE-2026-8054 dotCMS auditPublishing SQLi - SQL keyword in body"; \
flow:to_server,established; \
http.method; content:"POST"; \
http.uri; content:"/api/auditPublishing/getAll"; \
http.request_body; pcre:"/(?:INSERT|DELETE|UPDATE|DROP|UNION|COPY)\s/i"; \
reference:cve,2026-8054; \
classtype:web-application-attack; \
sid:2026805403; rev:1;)
# CVE-2026-8054: dotCMS Publish Audit API - unauthenticated access (pre-patch)
alert http any any -> any any (msg:"CVE-2026-8054 dotCMS auditPublishing unauthenticated access"; \
flow:to_server,established; \
http.uri; content:"/api/auditPublishing/"; \
http.header; content:!"Authorization"; \
reference:cve,2026-8054; \
classtype:web-application-attack; \
sid:2026805404; rev:1;)
Byte Offset Reference
The attack payload is embedded in the HTTP request body as a JSON array. The offsets are relative to the start of the HTTP body:
Offset Content Description
-----------------------------------------------------------------
0x00 [ JSON array open
0x01 " First string element open
0x02 x'); <SQL_PAYLOAD>-- Injection payload
N " String element close
N+1 ] JSON array close
The critical injection point is the content between the JSON string delimiters ("). Any single quote character (0x27) within these strings can break out of the SQL context.
7b. Host-Based Detection (YARA)
Since dotCMS is a Java application (WAR/JAR deployment), YARA rules target the compiled .class files rather than native PE binaries. The rules detect the vulnerable string concatenation pattern and the patched parameterized query pattern in the PublishAuditAPIImpl class file.
rule CVE_2026_8054_dotCMS_Vulnerable_PublishAuditAPIImpl {
meta:
description = "Detects vulnerable dotCMS PublishAuditAPIImpl with SQL string concatenation"
cve = "CVE-2026-8054"
component = "dotCMS Core Publish Audit API"
severity = "Critical"
type = "vulnerability"
strings:
// Vulnerable SQL template — string constant in class file
$sql_template = "select * from publishing_queue_audit where bundle_id in (%s)" ascii
// String concatenation pattern: wrapping in single quotes
$quote_wrap = "'" ascii
// Class identifier
$class_name = "PublishAuditAPIImpl" ascii
// Vulnerable method uses String.format with user input
$string_format = "String.format" ascii
condition:
$class_name and $sql_template and $string_format
}
rule CVE_2026_8054_dotCMS_Patched_PublishAuditAPIImpl {
meta:
description = "Detects patched dotCMS PublishAuditAPIImpl with parameterized queries"
cve = "CVE-2026-8054"
component = "dotCMS Core Publish Audit API"
severity = "Critical"
type = "patch_verification"
strings:
$class_name = "PublishAuditAPIImpl" ascii
$sql_template = "select * from publishing_queue_audit where bundle_id in (%s)" ascii
// Patched version uses addParam for JDBC parameter binding
$add_param = "addParam" ascii
// Auth check class referenced in patched endpoint
$auth_check = "AuthCredentialPushPublishUtil" ascii
condition:
$class_name and $sql_template and $add_param
}
Usage:
# Scan dotCMS installation for vulnerable class
find /opt/dotcms/ -name "*.class" -o -name "*.jar" | xargs yara -s cve_2026_8054.yar
# Scan WAR file directly (extract first)
jar xf dotcms.war && yara -r -s cve_2026_8054.yar WEB-INF/
| Rule | Match Means |
|---|---|
CVE_2026_8054_dotCMS_Vulnerable_PublishAuditAPIImpl | Vulnerable version detected — string concatenation in SQL query |
CVE_2026_8054_dotCMS_Patched_PublishAuditAPIImpl | Patched version detected — parameterized query with addParam binding |
8. References
| Source | URL |
|---|---|
| dotCMS Advisory (SI-75) | https://dev.dotcms.com/docs/known-security-issues?issueNumber=SI-75 |
| NVD | https://nvd.nist.gov/vuln/detail/CVE-2026-8054 |
| MITRE CVE | https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2026-8054 |
| Fix PR | https://github.com/dotCMS/core/pull/35553 |
| dotCMS Docker Hub | https://hub.docker.com/r/dotcms/dotcms |