From 88ea1d9b48f7ca4cacc09dd3886c01314e0028d6 Mon Sep 17 00:00:00 2001 From: DanBpx Date: Thu, 25 Jun 2026 15:32:51 +0300 Subject: [PATCH] Fix ClassCastException when adding headers to non-RequestWrapper requests The SDK blindly cast HttpServletRequest to RequestWrapper when injecting data enrichment, breached account, and additional S2S activity headers. This caused a ClassCastException for customers using their own HttpServletRequestWrapper implementations. Introduces addHeaderSafely() which checks instanceof before casting. If the request is not a RequestWrapper, falls back to request.setAttribute() so the data is still accessible to the customer's application. --- .../java/com/perimeterx/api/PerimeterX.java | 17 ++- .../api/DataEnrichmentHeaderTest.java | 142 ++++++++++++++++++ 2 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 perimeterx-sdk/src/test/java/com/perimeterx/api/DataEnrichmentHeaderTest.java diff --git a/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java b/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java index 42d7bd85..fe655fd6 100644 --- a/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java +++ b/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java @@ -242,9 +242,18 @@ private void addCustomHeadersToRequest(HttpServletRequest request, PXContext con setDataEnrichmentHeader(request, context); } + private void addHeaderSafely(HttpServletRequest request, String headerName, String headerValue, PXContext context) { + if (request instanceof RequestWrapper) { + ((RequestWrapper) request).addHeader(headerName, headerValue); + } else { + request.setAttribute(headerName, headerValue); + context.logger.debug("Request is not a RequestWrapper instance, header '{}' was added as a request attribute instead", headerName); + } + } + private void setBreachedAccount(HttpServletRequest request, PXContext context) { if (configuration.isLoginCredentialsExtractionEnabled() && context.isBreachedAccount()) { - ((RequestWrapper) request).addHeader(configuration.getPxCompromisedCredentialsHeader(), String.valueOf(context.getPxde().get(BREACHED_ACCOUNT_KEY_NAME))); + addHeaderSafely(request, configuration.getPxCompromisedCredentialsHeader(), String.valueOf(context.getPxde().get(BREACHED_ACCOUNT_KEY_NAME)), context); } } @@ -255,8 +264,8 @@ private void setAdditionalS2SActivityHeaders(HttpServletRequest request, PXConte final String stringifyActivity = new Gson().toJson(activity); final String urlHeader = configuration.getServerURL() + API_ACTIVITIES; - ((RequestWrapper) request).addHeader(ADDITIONAL_ACTIVITY_HEADER, stringifyActivity); - ((RequestWrapper) request).addHeader(ADDITIONAL_ACTIVITY_URL_HEADER, urlHeader); + addHeaderSafely(request, ADDITIONAL_ACTIVITY_HEADER, stringifyActivity, context); + addHeaderSafely(request, ADDITIONAL_ACTIVITY_URL_HEADER, urlHeader, context); } } @@ -274,7 +283,7 @@ private void setDataEnrichmentHeader(HttpServletRequest request, PXContext conte String pxdeJson = context.getPxde().toString(); byte[] utf8Bytes = pxdeJson.getBytes(StandardCharsets.UTF_8); String encodedPxde = new String(utf8Bytes, StandardCharsets.ISO_8859_1); - ((RequestWrapper) request).addHeader(headerName, encodedPxde); + addHeaderSafely(request, headerName, encodedPxde, context); } catch (Exception e) { context.logger.debug("Failed to add data enrichment header", e); } diff --git a/perimeterx-sdk/src/test/java/com/perimeterx/api/DataEnrichmentHeaderTest.java b/perimeterx-sdk/src/test/java/com/perimeterx/api/DataEnrichmentHeaderTest.java new file mode 100644 index 00000000..1dedfd6c --- /dev/null +++ b/perimeterx-sdk/src/test/java/com/perimeterx/api/DataEnrichmentHeaderTest.java @@ -0,0 +1,142 @@ +package com.perimeterx.api; + +import com.perimeterx.http.PXClient; +import com.perimeterx.http.RequestWrapper; +import com.perimeterx.models.PXContext; +import com.perimeterx.models.configuration.ModuleMode; +import com.perimeterx.models.configuration.PXConfiguration; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import testutils.TestObjectUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponseWrapper; + +@Test +public class DataEnrichmentHeaderTest { + + private static final String DE_HEADER_NAME = "X-PX-Data-Enrichment"; + + private PXConfiguration configuration; + + @BeforeMethod + public void setup() { + configuration = PXConfiguration.builder() + .appId("appId") + .authToken("token") + .cookieKey("cookieKey") + .moduleMode(ModuleMode.BLOCKING) + .remoteConfigurationEnabled(false) + .blockingScore(70) + .pxDataEnrichmentHeaderName(DE_HEADER_NAME) + .build(); + } + + /** + * Simulates the customer's scenario: a custom HttpServletRequestWrapper + * that is NOT our RequestWrapper. Before the fix, this would throw a + * ClassCastException inside setDataEnrichmentHeader. + */ + @Test + public void testNonRequestWrapper_oldBehaviorWouldThrowClassCastException() { + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + HttpServletRequest customWrapper = new HttpServletRequestWrapper(mockRequest); + + Assert.assertFalse(customWrapper instanceof RequestWrapper, + "Test precondition: the custom wrapper must NOT be a RequestWrapper"); + + boolean wouldThrow = false; + try { + ((RequestWrapper) customWrapper).addHeader(DE_HEADER_NAME, "test"); + } catch (ClassCastException e) { + wouldThrow = true; + } + Assert.assertTrue(wouldThrow, + "Direct cast to RequestWrapper should throw ClassCastException for non-RequestWrapper requests"); + } + + /** + * With the fix: passing a non-RequestWrapper request to pxVerify should + * NOT throw, and the DE data should be available via request.getAttribute(). + */ + @Test + public void testNonRequestWrapper_dataEnrichmentSetAsAttribute() throws Exception { + PXClient client = TestObjectUtils.nonBlockingPXClient(configuration.getBlockingScore()); + PerimeterX perimeterx = TestObjectUtils.testablePerimeterXObject(configuration, client); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + HttpServletRequest customWrapper = new HttpServletRequestWrapper(mockRequest); + + Assert.assertFalse(customWrapper instanceof RequestWrapper, + "Test precondition: must not be a RequestWrapper"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + PXContext context = perimeterx.pxVerify(customWrapper, new HttpServletResponseWrapper(response)); + + Assert.assertNotNull(context, "pxVerify should not return null"); + Assert.assertNotEquals(response.getStatus(), 403, "Request should not be blocked"); + + String deValue = (String) customWrapper.getAttribute(DE_HEADER_NAME); + Assert.assertNotNull(deValue, + "Data enrichment should be available as a request attribute when request is not a RequestWrapper"); + Assert.assertTrue(deValue.contains("cookieMonster"), + "Data enrichment attribute should contain the expected PXDE data"); + } + + /** + * With RequestWrapper: existing behavior is preserved. + * The DE data should be available via request.getHeader(). + */ + @Test + public void testRequestWrapper_dataEnrichmentSetAsHeader() throws Exception { + PXClient client = TestObjectUtils.nonBlockingPXClient(configuration.getBlockingScore()); + PerimeterX perimeterx = TestObjectUtils.testablePerimeterXObject(configuration, client); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + RequestWrapper wrappedRequest = new RequestWrapper(mockRequest); + + MockHttpServletResponse response = new MockHttpServletResponse(); + PXContext context = perimeterx.pxVerify(wrappedRequest, new HttpServletResponseWrapper(response)); + + Assert.assertNotNull(context, "pxVerify should not return null"); + Assert.assertNotEquals(response.getStatus(), 403, "Request should not be blocked"); + + String deHeader = wrappedRequest.getHeader(DE_HEADER_NAME); + Assert.assertNotNull(deHeader, + "Data enrichment should be available as a request header when using RequestWrapper"); + Assert.assertTrue(deHeader.contains("cookieMonster"), + "Data enrichment header should contain the expected PXDE data"); + } + + /** + * When DE header is not configured, neither header nor attribute should be set, + * regardless of request type. + */ + @Test + public void testNoHeaderConfigured_nothingSet() throws Exception { + PXConfiguration noDeConfig = PXConfiguration.builder() + .appId("appId") + .authToken("token") + .cookieKey("cookieKey") + .moduleMode(ModuleMode.BLOCKING) + .remoteConfigurationEnabled(false) + .blockingScore(70) + .build(); + + PXClient client = TestObjectUtils.nonBlockingPXClient(noDeConfig.getBlockingScore()); + PerimeterX perimeterx = TestObjectUtils.testablePerimeterXObject(noDeConfig, client); + + MockHttpServletRequest mockRequest = new MockHttpServletRequest(); + HttpServletRequest customWrapper = new HttpServletRequestWrapper(mockRequest); + + MockHttpServletResponse response = new MockHttpServletResponse(); + perimeterx.pxVerify(customWrapper, new HttpServletResponseWrapper(response)); + + Assert.assertNull(customWrapper.getAttribute(DE_HEADER_NAME), + "No attribute should be set when DE header name is not configured"); + } +}