VYPR
Critical severityNVD Advisory· Published May 27, 2026

CVE-2026-8054

CVE-2026-8054

Description

Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection') in the Publish Audit API endpoints (/api/auditPublishing/get and /api/auditPublishing/getAll) in dotCMS Core 25.11.04-1 through 26.04.28-02 allows remote unauthenticated attackers to read, modify, or destroy arbitrary database content. The endpoints did not enforce authentication and accepted unsanitized input used in dynamically constructed SQL. The fix in dotCMS Core 26.04.28-03 requires an authenticated backend user with the publishing-queue portlet permission. LTS releases are not affected as the vulnerable code path was never backported.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Unauthenticated SQL injection in dotCMS Core publish audit API endpoints allows remote attackers to read, modify, or destroy database content.

Vulnerability

An SQL injection vulnerability exists in dotCMS Core versions 25.11.04-1 through 26.04.28-02 in the Publish Audit API endpoints /api/auditPublishing/get and /api/auditPublishing/getAll. The endpoints do not enforce authentication and directly use unsanitized input from bundle-id parameters in dynamically constructed SQL queries [1]. The vulnerable code path was never backported to LTS releases.

Exploitation

A remote unauthenticated attacker can exploit this by sending crafted HTTP requests to the vulnerable endpoints with malicious SQL payloads in the bundle-id parameter. No authentication, user interaction, or prior access is required. The lack of input validation allows the attacker to break out of SQL literals [1].

Impact

Successful exploitation enables an attacker to read, modify, or destroy arbitrary database content. This includes data exfiltration, unauthorized modification of records, or complete deletion of database tables, leading to full compromise of data confidentiality, integrity, and availability [1].

Mitigation

The fix was released in dotCMS Core version 26.04.28-03. The update requires an authenticated backend user with the publishing-queue portlet permission to access these endpoints and parameterizes the SQL queries to prevent injection [1]. Users running affected versions must upgrade to 26.04.28-03 or later. LTS releases are not vulnerable.

AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
6a5f4188715b

fix(publisher): parameterize getPublishAuditStatuses bundle-id query (#35553)

https://github.com/dotcms/coreMehdiMay 6, 2026via nvd-ref
8 files changed · +207 17
  • dotcms-integration/src/test/java/com/dotcms/MainSuite2a.java+2 1 modified
    @@ -105,7 +105,8 @@
             com.dotmarketing.portlets.categories.business.CategoryAPITest.class,
             com.dotmarketing.filters.FiltersTest.class,
             InterceptorHandlerTest.class,
    -        com.dotcms.graphql.datafetcher.page.NumberContentsDataFetcherTest.class
    +        com.dotcms.graphql.datafetcher.page.NumberContentsDataFetcherTest.class,
    +        com.dotcms.rest.AuditPublishingResourceTest.class,
     })
     public class MainSuite2a {
     
    
  • dotcms-integration/src/test/java/com/dotcms/publisher/business/PublishAuditAPITest.java+96 0 modified
    @@ -19,6 +19,7 @@
     
     import java.util.ArrayList;
     import java.util.Arrays;
    +import java.util.Collections;
     import java.util.HashMap;
     import java.util.List;
     import java.util.Map;
    @@ -386,6 +387,101 @@ public void allThePublishAuditHistoryWithOneAssets() throws DotPublisherExceptio
                     });
         }
     
    +    /**
    +     * Method to test: {@link PublishAuditAPI#getPublishAuditStatuses(List)}
    +     * When: three audit statuses are inserted and two of them are requested by id
    +     * Should: return exactly the two requested rows.
    +     */
    +    @Test
    +    public void test_getPublishAuditStatuses_returnsRequestedRows() throws DotPublisherException {
    +        final String bundleId1 = UUIDGenerator.generateUuid();
    +        final String bundleId2 = UUIDGenerator.generateUuid();
    +        final String bundleId3 = UUIDGenerator.generateUuid();
    +        insertPublishAuditStatus(Status.SUCCESS, bundleId1);
    +        insertPublishAuditStatus(Status.FAILED_TO_PUBLISH, bundleId2);
    +        insertPublishAuditStatus(Status.SUCCESS, bundleId3);
    +
    +        try {
    +            final List<PublishAuditStatus> result = publishAuditAPI.getPublishAuditStatuses(
    +                    Arrays.asList(bundleId1, bundleId2));
    +
    +            assertNotNull(result);
    +            assertEquals(2, result.size());
    +            final Set<String> ids = result.stream()
    +                    .map(PublishAuditStatus::getBundleId)
    +                    .collect(Collectors.toSet());
    +            assertTrue(ids.contains(bundleId1));
    +            assertTrue(ids.contains(bundleId2));
    +            assertFalse(ids.contains(bundleId3));
    +        } finally {
    +            publishAuditAPI.deletePublishAuditStatus(bundleId1);
    +            publishAuditAPI.deletePublishAuditStatus(bundleId2);
    +            publishAuditAPI.deletePublishAuditStatus(bundleId3);
    +        }
    +    }
    +
    +    /**
    +     * Method to test: {@link PublishAuditAPI#getPublishAuditStatuses(List)}
    +     * When: an empty list is passed
    +     * Should: return an empty result, not throw or produce invalid SQL.
    +     */
    +    @Test
    +    public void test_getPublishAuditStatuses_emptyList_returnsEmpty() throws DotPublisherException {
    +        final List<PublishAuditStatus> result = publishAuditAPI.getPublishAuditStatuses(Collections.emptyList());
    +        assertNotNull(result);
    +        assertTrue(result.isEmpty());
    +    }
    +
    +    /**
    +     * Method to test: {@link PublishAuditAPI#getPublishAuditStatuses(List)}
    +     * When: null is passed
    +     * Should: return an empty result, not throw NPE.
    +     */
    +    @Test
    +    public void test_getPublishAuditStatuses_nullList_returnsEmpty() throws DotPublisherException {
    +        final List<PublishAuditStatus> result = publishAuditAPI.getPublishAuditStatuses(null);
    +        assertNotNull(result);
    +        assertTrue(result.isEmpty());
    +    }
    +
    +    /**
    +     * Method to test: {@link PublishAuditAPI#getPublishAuditStatuses(List)}
    +     * When: bundle ids contain SQL meta-characters (single quotes, OR 1=1, ; DROP TABLE)
    +     * Should: bind the values as parameters, return an empty result, leave the
    +     *         publishing_queue_audit table intact.
    +     *
    +     * Regression test for the previous String.format-based IN-clause that
    +     * concatenated quote-wrapped ids directly into the SQL.
    +     */
    +    @Test
    +    public void test_getPublishAuditStatuses_neutralizesSqlInjectionAttempts() throws DotPublisherException {
    +        final String realBundleId = UUIDGenerator.generateUuid();
    +        insertPublishAuditStatus(Status.SUCCESS, realBundleId);
    +
    +        try {
    +            final List<String> maliciousIds = Arrays.asList(
    +                    "x' OR '1'='1",
    +                    "x'; DROP TABLE publishing_queue_audit; --",
    +                    "x' UNION SELECT '" + realBundleId + "' --",
    +                    "x'/*",
    +                    "x\\' OR \\'1\\'=\\'1"
    +            );
    +
    +            final List<PublishAuditStatus> result = publishAuditAPI.getPublishAuditStatuses(maliciousIds);
    +
    +            assertNotNull(result);
    +            assertTrue("Malicious bundle ids should not match any rows; got "
    +                    + result.size() + " rows back, indicating values were not bound as parameters",
    +                    result.isEmpty());
    +
    +            final PublishAuditStatus stillThere = publishAuditAPI.getPublishAuditStatus(realBundleId);
    +            assertNotNull("publishing_queue_audit row was lost after SQL injection attempt — "
    +                    + "DROP TABLE payload may have executed", stillThere);
    +        } finally {
    +            publishAuditAPI.deletePublishAuditStatus(realBundleId);
    +        }
    +    }
    +
         @Test
         public void test_getPublishAuditStatusByFilter()
                 throws DotPublisherException, DotDataException {
    
  • dotcms-integration/src/test/java/com/dotcms/rest/AuditPublishingResourceTest.java+51 0 added
    @@ -0,0 +1,51 @@
    +package com.dotcms.rest;
    +
    +import com.dotcms.IntegrationTestBase;
    +import com.dotcms.util.IntegrationTestInitService;
    +import org.junit.BeforeClass;
    +import org.junit.Test;
    +
    +import javax.servlet.http.HttpServletRequest;
    +import javax.ws.rs.core.Response;
    +import java.util.Collections;
    +
    +import static org.junit.Assert.assertEquals;
    +import static org.mockito.Mockito.mock;
    +
    +/**
    + * Verifies that {@link AuditPublishingResource} enforces push-publish token
    + * authentication on its endpoints. Both methods were previously reachable
    + * anonymously; they now require a valid PP auth token.
    + */
    +public class AuditPublishingResourceTest extends IntegrationTestBase {
    +
    +    @BeforeClass
    +    public static void prepare() throws Exception {
    +        IntegrationTestInitService.getInstance().init();
    +    }
    +
    +    /**
    +     * Method to test: {@link AuditPublishingResource#get(String, HttpServletRequest)}
    +     * When: called with a request that carries no push-publish auth token
    +     * Should: return 401 Unauthorized.
    +     */
    +    @Test
    +    public void test_get_rejectsAnonymousRequest() {
    +        final HttpServletRequest request = mock(HttpServletRequest.class);
    +        final Response response = new AuditPublishingResource().get("any-bundle-id", request);
    +        assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
    +    }
    +
    +    /**
    +     * Method to test: {@link AuditPublishingResource#getAll(java.util.List, HttpServletRequest)}
    +     * When: called with a request that carries no push-publish auth token
    +     * Should: return 401 Unauthorized.
    +     */
    +    @Test
    +    public void test_getAll_rejectsAnonymousRequest() {
    +        final HttpServletRequest request = mock(HttpServletRequest.class);
    +        final Response response = new AuditPublishingResource()
    +                .getAll(Collections.singletonList("any-bundle-id"), request);
    +        assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
    +    }
    +}
    \ No newline at end of file
    
  • dotcms-postman/src/main/resources/postman/Push_Publish_JWT_Token_Test.postman_collection.json+10 0 modified
    @@ -284,6 +284,16 @@
     						}
     					],
     					"request": {
    +						"auth": {
    +                        	"type": "bearer",
    +                        	"bearer": [
    +								{
    +									"key": "token",
    +                                  	"value": "{{token}}",
    +                            		"type": "string"
    +								}
    +							]
    +						},
     						"method": "GET",
     						"header": [],
     						"url": {
    
  • dotCMS/src/main/java/com/dotcms/publisher/business/PublishAuditAPIImpl.java+16 9 modified
    @@ -16,6 +16,7 @@
     
     import java.util.ArrayList;
     import java.util.Arrays;
    +import java.util.Collections;
     import java.util.Date;
     import java.util.List;
     import java.util.Map;
    @@ -224,23 +225,29 @@ public PublishAuditStatus getPublishAuditStatus(String bundleId)
     	@CloseDBIfOpened
     	public List<PublishAuditStatus> getPublishAuditStatuses(List<String> bundleIds)
                 throws DotPublisherException {
    +		if (bundleIds == null || bundleIds.isEmpty()) {
    +			return Collections.emptyList();
    +		}
     		try {
     			final List<PublishAuditStatus> result = new ArrayList<>();
     
    -			DotConnect dc = new DotConnect();
    -			final List<String> parameter = bundleIds.stream().map(id -> "'" + id + "'").collect(Collectors.toList());
    +			final DotConnect dc = new DotConnect();
    +			final String placeholders = bundleIds.stream()
    +					.map(id -> "?")
    +					.collect(Collectors.joining(","));
     
    -			dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS,  String.join(",", parameter)));
    -			List<Map<String, Object>> items = dc.loadObjectResults();
    +			dc.setSQL(String.format(SELECT_ALL_BY_BUNDLES_IDS, placeholders));
    +			bundleIds.forEach(dc::addParam);
    +			final List<Map<String, Object>> items = dc.loadObjectResults();
     
    -			for(Map<String, Object> item: items) {
    -				result.add(turnIntoPublishAuditStatus(NO_LIMIT_ASSETS,  item));
    +			for (final Map<String, Object> item : items) {
    +				result.add(turnIntoPublishAuditStatus(NO_LIMIT_ASSETS, item));
     			}
     
     			return result;
    -		}catch(Exception e){
    -			Logger.debug(PublisherUtil.class,e.getMessage(),e);
    -			throw new DotPublisherException("Unable to get list of elements with error:"+e.getMessage(), e);
    +		} catch (Exception e) {
    +			Logger.error(PublishAuditAPIImpl.class, e.getMessage(), e);
    +			throw new DotPublisherException("Unable to get list of elements with error:" + e.getMessage(), e);
     		}
     
     	}
    
  • dotCMS/src/main/java/com/dotcms/publisher/business/PublisherQueueJob.java+2 0 modified
    @@ -15,6 +15,7 @@
     import com.dotcms.publisher.endpoint.business.PublishingEndPointAPI;
     import com.dotcms.publisher.environment.bean.Environment;
     import com.dotcms.publisher.environment.business.EnvironmentAPI;
    +import com.dotcms.publisher.pusher.AuthCredentialPushPublishUtil;
     import com.dotcms.publisher.pusher.PushPublisher;
     import com.dotcms.publisher.pusher.PushPublisherConfig;
     import com.dotcms.publisher.util.PublisherUtil;
    @@ -653,6 +654,7 @@ private List<PublishAuditHistory> getRemoteHistoryFromEndpoint(final  List<Strin
     
     		final String responseBody = webTarget
     				.request(MediaType.APPLICATION_JSON)
    +				.header("Authorization", AuthCredentialPushPublishUtil.INSTANCE.getRequestToken(targetEndpoint).orElse(""))
     				.post(Entity.entity(bundleIds, MediaType.APPLICATION_JSON))
     				.readEntity(String.class);
     
    
  • dotCMS/src/main/java/com/dotcms/publisher/pusher/AuthCredentialPushPublishUtil.java+2 2 modified
    @@ -154,9 +154,9 @@ private String getTokenFromRequest(final HttpServletRequest request) {
                     .startsWith(BEARER)) {
     
                 return authorizationHeader.substring(BEARER.length());
    -        } else {
    -            throw new IllegalArgumentException("Bearer Authorization header expected");
             }
    +
    +        return StringUtils.EMPTY;
         }
     
         public static class PushPublishAuthenticationToken {
    
  • dotCMS/src/main/java/com/dotcms/rest/AuditPublishingResource.java+28 5 modified
    @@ -3,18 +3,19 @@
     import com.dotcms.publisher.business.DotPublisherException;
     import com.dotcms.publisher.business.PublishAuditAPI;
     import com.dotcms.publisher.business.PublishAuditStatus;
    +import com.dotcms.publisher.pusher.AuthCredentialPushPublishUtil;
     
    +import javax.servlet.http.HttpServletRequest;
     import javax.ws.rs.*;
    +import javax.ws.rs.core.Context;
     import javax.ws.rs.core.MediaType;
     import javax.ws.rs.core.Response;
     
    -import com.dotmarketing.util.Config;
     import com.dotmarketing.util.Logger;
    -import com.google.common.collect.Lists;
    -import io.swagger.v3.oas.annotations.parameters.RequestBody;
     import io.swagger.v3.oas.annotations.tags.Tag;
     
     import java.util.List;
    +import java.util.Optional;
     
     @Path("/auditPublishing")
     @Tag(name = "Publishing")
    @@ -24,7 +25,18 @@ public class AuditPublishingResource {
         @GET
         @Path("/get/{bundleId:.*}")
         @Produces(MediaType.TEXT_XML)
    -    public Response get(@PathParam("bundleId") String bundleId) {
    +    public Response get(@PathParam("bundleId") final String bundleId,
    +                        @Context final HttpServletRequest request) {
    +
    +        final AuthCredentialPushPublishUtil.PushPublishAuthenticationToken ppAuthToken =
    +                AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request);
    +
    +        final Optional<Response> failResponse = PushPublishResourceUtil.getFailResponse(request, ppAuthToken);
    +
    +        if (failResponse.isPresent()) {
    +            return failResponse.get();
    +        }
    +
             PublishAuditStatus status = null;
     
             try {
    @@ -42,7 +54,18 @@ public Response get(@PathParam("bundleId") String bundleId) {
         @POST
         @Path("/getAll")
         @Produces(MediaType.APPLICATION_JSON)
    -    public Response getAll( List<String> bundleIds) {
    +    public Response getAll(final List<String> bundleIds,
    +                           @Context final HttpServletRequest request) {
    +
    +        final AuthCredentialPushPublishUtil.PushPublishAuthenticationToken ppAuthToken =
    +                AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request);
    +
    +        final Optional<Response> failResponse = PushPublishResourceUtil.getFailResponse(request, ppAuthToken);
    +
    +        if (failResponse.isPresent()) {
    +            return failResponse.get();
    +        }
    +
             try {
                 final List<PublishAuditStatus> statuses = auditAPI.getPublishAuditStatuses(bundleIds);
     
    

Vulnerability mechanics

Root cause

"Missing input sanitization and parameter binding in SQL query construction, combined with absent authentication enforcement on the REST endpoints, allows unauthenticated SQL injection."

Attack vector

An unauthenticated remote attacker sends a GET request to `/api/auditPublishing/get/{bundleId}` or a POST request to `/api/auditPublishing/getAll` with a crafted bundle id value containing SQL meta-characters such as single quotes, `OR 1=1`, or `; DROP TABLE` statements [ref_id=1]. Because the endpoints did not enforce authentication and the `getPublishAuditStatuses` method concatenated unsanitized input directly into the SQL query string, the attacker can read, modify, or destroy arbitrary database content [patch_id=2651066]. The only precondition is network access to a dotCMS instance running a vulnerable version (25.11.04-1 through 26.04.28-02).

Affected code

The vulnerable code is in `PublishAuditAPIImpl.getPublishAuditStatuses(List<String>)` which built an SQL `IN (...)` clause by wrapping each caller-supplied bundle id in single quotes and using `String.format` to concatenate them directly into the query string — no escaping or parameter binding [patch_id=2651066]. The two REST endpoints that call this method, `AuditPublishingResource.get()` (`/api/auditPublishing/get`) and `AuditPublishingResource.getAll()` (`/api/auditPublishing/getAll`), previously performed no authentication check, allowing unauthenticated remote callers to reach the SQL injection point [ref_id=1].

What the fix does

The patch replaces the manually quoted IN-clause with `?` placeholders bound via `DotConnect.addParam(...)`, preventing SQL injection by ensuring caller-supplied values are never interpreted as SQL syntax [patch_id=2651066]. It also adds a null/empty guard that returns `Collections.emptyList()` early, avoiding NPEs and invalid `IN ()` SQL. Additionally, both REST endpoints now require authentication via `AuthCredentialPushPublishUtil.INSTANCE.processAuthHeader(request)` and reject unauthenticated requests with a 401 response, closing the anonymous access path [patch_id=2651066].

Preconditions

  • networkNetwork access to a vulnerable dotCMS instance (versions 25.11.04-1 through 26.04.28-02)
  • authNo authentication required — the vulnerable endpoints accepted anonymous requests

Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.