From 88ea1d9b48f7ca4cacc09dd3886c01314e0028d6 Mon Sep 17 00:00:00 2001 From: DanBpx Date: Thu, 25 Jun 2026 15:32:51 +0300 Subject: [PATCH 1/4] 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"); + } +} From 5269c5159657d2b15b78b38eea981c99d19eb187 Mon Sep 17 00:00:00 2001 From: DanBpx Date: Mon, 29 Jun 2026 11:24:40 +0300 Subject: [PATCH 2/4] Remove debug log from addHeaderSafely fallback path --- perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java | 1 - 1 file changed, 1 deletion(-) 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 fe655fd6..35b669f2 100644 --- a/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java +++ b/perimeterx-sdk/src/main/java/com/perimeterx/api/PerimeterX.java @@ -247,7 +247,6 @@ private void addHeaderSafely(HttpServletRequest request, String headerName, Stri ((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); } } From d6526c290755d0f26f0b75770215daef570e8c9f Mon Sep 17 00:00:00 2001 From: DanBpx Date: Mon, 29 Jun 2026 12:45:57 +0300 Subject: [PATCH 3/4] release v6.17.1 --- CHANGELOG.md | 4 ++++ README.md | 2 +- perimeterx-sdk-jakarta/pom.xml | 2 +- perimeterx-sdk/pom.xml | 2 +- pom.xml | 2 +- px_metadata.json | 2 +- web-jakarta/pom.xml | 2 +- 7 files changed, 10 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 216894ff..fb33e2aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change Log +## [v6.17.1](https://github.com/PerimeterX/perimeterx-java-sdk/compare/v6.17.1...HEAD) (2026-06-29) +- Fix ClassCastException when adding headers to non-RequestWrapper request + + ## [v6.17.0](https://github.com/PerimeterX/perimeterx-java-sdk/compare/v6.17.0...HEAD) (2026-05-24) - Added Jakarta EE support via new `perimeterx-sdk-jakarta` artifact (Spring Boot 3, Tomcat 10+) - Fixed first party captcha reverse proxy to use `startsWith` instead of `contains` when matching the captcha prefix diff --git a/README.md b/README.md index ec46f2a0..c4915878 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # [PerimeterX](http://www.perimeterx.com) Java SDK -> Latest stable version: [v6.17.0](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.17.0%7Cjar) +> Latest stable version: [v6.17.1](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.17.0%7Cjar) ## Table of Contents diff --git a/perimeterx-sdk-jakarta/pom.xml b/perimeterx-sdk-jakarta/pom.xml index 76dac18e..93fde185 100644 --- a/perimeterx-sdk-jakarta/pom.xml +++ b/perimeterx-sdk-jakarta/pom.xml @@ -8,7 +8,7 @@ com.perimeterx perimeterx-java-sdk-parent - 6.17.0 + 6.17.1 ../pom.xml diff --git a/perimeterx-sdk/pom.xml b/perimeterx-sdk/pom.xml index 1d4f78a2..ecc00c9a 100644 --- a/perimeterx-sdk/pom.xml +++ b/perimeterx-sdk/pom.xml @@ -8,7 +8,7 @@ com.perimeterx perimeterx-java-sdk-parent - 6.17.0 + 6.17.1 ../pom.xml diff --git a/pom.xml b/pom.xml index 4cfcceb1..11dedb50 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ 4.0.0 com.perimeterx perimeterx-java-sdk-parent - 6.17.0 + 6.17.1 pom PerimeterX Java SDK Parent diff --git a/px_metadata.json b/px_metadata.json index 9c94afdb..f3b282f6 100644 --- a/px_metadata.json +++ b/px_metadata.json @@ -1,5 +1,5 @@ { - "version": "6.17.0", + "version": "6.17.1", "supported_features": [ "advanced_blocking_response", "batched_activities", diff --git a/web-jakarta/pom.xml b/web-jakarta/pom.xml index ccfa5071..423d5a22 100644 --- a/web-jakarta/pom.xml +++ b/web-jakarta/pom.xml @@ -12,7 +12,7 @@ 17 - 6.17.0 + 6.17.1 From e45a939489687c496661a433b4feb266d2f62ca4 Mon Sep 17 00:00:00 2001 From: DanBpx Date: Mon, 29 Jun 2026 14:10:02 +0300 Subject: [PATCH 4/4] fix versioning --- README.md | 2 +- web/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c4915878..df5c04b5 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # [PerimeterX](http://www.perimeterx.com) Java SDK -> Latest stable version: [v6.17.1](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.17.0%7Cjar) +> Latest stable version: [v6.17.1](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.17.1%7Cjar) ## Table of Contents diff --git a/web/pom.xml b/web/pom.xml index 9ff1d9ef..0cb72217 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -13,7 +13,7 @@ 8 8 - 6.17.0 + 6.17.1