diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 830f54a6..e99e0760 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -11,11 +11,11 @@ jobs: version: ${{ steps.version.outputs.value }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '18.x' + node-version: '22.x' - name: Get package version id: version run: echo "value=$(node -p -e "require('./px_metadata.json').version")" >> "$GITHUB_OUTPUT" @@ -28,7 +28,7 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - run: gh release create v${{ needs.extract_version.outputs.version }} --generate-notes -t "Version ${{ needs.extract_version.outputs.version }}" env: GITHUB_TOKEN: ${{ github.TOKEN }} @@ -42,7 +42,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up JDK 8 uses: actions/setup-java@v3 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b29753ae..c541b8b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Set up JDK 8 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/ci_e2e.yaml b/.github/workflows/ci_e2e.yaml index f3e6a9bf..3a8e52e6 100644 --- a/.github/workflows/ci_e2e.yaml +++ b/.github/workflows/ci_e2e.yaml @@ -12,11 +12,11 @@ jobs: supported-features: ${{ steps.supported-features.outputs.value }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '20.x' + node-version: '22.x' - name: extract supported features id: supported-features run: echo "value=$(node -p -e "require('./px_metadata.json').supported_features?.join(' or ') || ''")" >> "$GITHUB_OUTPUT" @@ -25,9 +25,9 @@ jobs: CI: name: "E2E tests" env: - MOCK_COLLECTOR_IMAGE_TAG: 1.3.5 + MOCK_COLLECTOR_IMAGE_TAG: 2.0.6 SAMPLE_SITE_IMAGE_TAG: 1.0.0 - ENFORCER_SPEC_TESTS_IMAGE_TAG: 1.8.1 + ENFORCER_SPEC_TESTS_IMAGE_TAG: 1.23.3 runs-on: ubuntu-latest timeout-minutes: 60 @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker uses: docker/setup-buildx-action@v3 @@ -50,34 +50,34 @@ jobs: docker build . -t localhost:5001/java-enforcer-sample-site:$SAMPLE_SITE_IMAGE_TAG && \ docker push localhost:5001/java-enforcer-sample-site:$SAMPLE_SITE_IMAGE_TAG - - uses: azure/setup-helm@v3 + - uses: azure/setup-helm@v4 with: - version: '3.14.1' + version: '3.19.0' - name: Clone helm charts repo - mock-collector - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: PerimeterX/connect-helm-charts token: ${{ secrets.CONNECT_PULL_TOKEN }} - ref: mock-collector-0.1.1 + ref: mock-collector-0.1.2 path: ./deploy_charts/mock-collector - name: Clone helm charts repo - enforcer-tests - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: PerimeterX/connect-helm-charts token: ${{ secrets.CONNECT_PULL_TOKEN }} - ref: enforcer-spec-tests-0.7.1 + ref: enforcer-spec-tests-0.9.1 path: ./deploy_charts/enforcer-spec-tests - name: Clone helm charts repo - sample-site - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: PerimeterX/connect-helm-charts token: ${{ secrets.CONNECT_PULL_TOKEN }} - ref: sample-site-0.5.0 + ref: sample-site-0.6.1 path: ./deploy_charts/sample-site - name: Set up Google Cloud SDK @@ -101,6 +101,7 @@ jobs: helm install mock-collector ./deploy_charts/mock-collector/charts/mock-collector \ --set image.repository=localhost:5001/mock-collector \ --set image.tag=$MOCK_COLLECTOR_IMAGE_TAG \ + --set authToken=${{ secrets.PX_AUTH_TOKEN }} \ --set imagePullPolicy=Always --wait - name: set secrets in enforcer config @@ -108,7 +109,8 @@ jobs: cat ./ci_files/enforcer-config.json |\ jq '.px_app_id="${{ secrets.PX_APP_ID }}"' |\ jq '.px_cookie_secret="${{ secrets.TEST_COOKIE_SECRET }}"' |\ - jq '.px_auth_token="${{ secrets.PX_AUTH_TOKEN }}"' > /tmp/enforcer-config.json + jq '.px_auth_token="${{ secrets.PX_AUTH_TOKEN }}"' |\ + jq '.px_logger_auth_token="${{ secrets.PX_LOGGER_AUTH_TOKEN }}"' > /tmp/enforcer-config.json - name: log enforcer config run: cat /tmp/enforcer-config.json @@ -118,6 +120,9 @@ jobs: helm install java-enforcer ./deploy_charts/sample-site/charts/sample-site \ -f ./ci_files/enforcer-values.yaml \ --set image.name=localhost:5001/java-enforcer-sample-site \ + --set appId=${{ secrets.PX_APP_ID }} \ + --set cookieSecret=${{ secrets.TEST_COOKIE_SECRET }} \ + --set authToken=${{ secrets.PX_AUTH_TOKEN }} \ --set image.tag=$SAMPLE_SITE_IMAGE_TAG \ --set-file enforcerConfig.content=/tmp/enforcer-config.json \ --wait @@ -137,6 +142,7 @@ jobs: --set authToken="${{ secrets.PX_AUTH_TOKEN }}" \ --set appId=${{ secrets.PX_APP_ID }} \ --set-file enforcerMetadataContent=./px_metadata.json \ + --set-file enforcerConfigJsonContent=/tmp/enforcer-config.json \ -f ./ci_files/spec-tests-values.yaml \ --wait \ --timeout 60m0s \ @@ -145,3 +151,7 @@ jobs: - name: get tests results if: ${{ always() }} run: kubectl logs job/enforcer-spec-tests + + - name: get enforcer logs + if: ${{ always() }} + run: kubectl logs deployment/java-enforcer-sample-site diff --git a/.github/workflows/ci_verify_version.yaml b/.github/workflows/ci_verify_version.yaml index 9fcd468a..64a18a71 100644 --- a/.github/workflows/ci_verify_version.yaml +++ b/.github/workflows/ci_verify_version.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - ${{ github.base_ref }} - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.base_ref }} @@ -18,7 +18,7 @@ jobs: run: echo "project=$( mvn help:evaluate -Dexpression=project.version -q -DforceStdout )" >> "$GITHUB_OUTPUT" - name: Checkout code - current commit - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Get current SDK versions id: new-version diff --git a/.github/workflows/docs_enforcement.yml b/.github/workflows/docs_enforcement.yml index 213c85c7..22fa0a2c 100644 --- a/.github/workflows/docs_enforcement.yml +++ b/.github/workflows/docs_enforcement.yml @@ -16,12 +16,12 @@ jobs: version: ${{ steps.version.outputs.value }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: '22' + node-version: '22.x' - name: Get package version id: version diff --git a/.github/workflows/fuzzer.yaml b/.github/workflows/fuzzer.yaml index 4c939349..c703e737 100644 --- a/.github/workflows/fuzzer.yaml +++ b/.github/workflows/fuzzer.yaml @@ -12,11 +12,11 @@ jobs: supported-features: ${{ steps.version.outputs.value }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 'latest' + node-version: '22.x' - name: Get package version id: version run: echo "value=$(node -p -e "require('./px_metadata.json').version")" >> "$GITHUB_OUTPUT" @@ -25,8 +25,8 @@ jobs: Fuzzing: name: "Fuzzing Test" env: - MOCK_COLLECTOR_IMAGE_TAG: 1.3.6 - FUZZER_TAG: 1.0.4 + MOCK_COLLECTOR_IMAGE_TAG: 2.0.6 + FUZZER_TAG: 1.1.0 SAMPLE_SITE_IMAGE_TAG: 1.0.0 ENFORCER_TAG: ${{ needs.extract_version.outputs.version }} @@ -42,7 +42,7 @@ jobs: steps: - name: Checkout Repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Docker uses: docker/setup-buildx-action@v3 @@ -55,32 +55,32 @@ jobs: docker build . -t localhost:5001/java-enforcer-sample-site:$SAMPLE_SITE_IMAGE_TAG && \ docker push localhost:5001/java-enforcer-sample-site:$SAMPLE_SITE_IMAGE_TAG - - uses: azure/setup-helm@v3 + - uses: azure/setup-helm@v4 with: - version: '3.14.2' + version: '3.19.0' - name: Clone helm charts repo - mock-collector - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: PerimeterX/connect-helm-charts token: ${{ secrets.CONNECT_PULL_TOKEN }} - ref: mock-collector-0.1.1 + ref: mock-collector-0.1.2 path: ./deploy_charts/mock-collector - name: Clone helm charts repo - fuzzer - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: PerimeterX/connect-helm-charts token: ${{ secrets.CONNECT_PULL_TOKEN }} - ref: fuzzer-0.2.0 + ref: fuzzer-0.3.1 path: ./deploy_charts/fuzzer - name: Clone helm charts repo - sample-site - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: PerimeterX/connect-helm-charts token: ${{ secrets.CONNECT_PULL_TOKEN }} - ref: sample-site-0.5.0 + ref: sample-site-0.6.1 path: ./deploy_charts/sample-site - name: Set up Google Cloud SDK @@ -104,14 +104,17 @@ jobs: helm install mock-collector ./deploy_charts/mock-collector/charts/mock-collector \ --set image.repository=localhost:5001/mock-collector \ --set image.tag=$MOCK_COLLECTOR_IMAGE_TAG \ - --set imagePullPolicy=Always --wait + --set authToken=${{ secrets.PX_AUTH_TOKEN }} \ + --set imagePullPolicy=Always \ + --wait - name: set secrets in enforcer config run: | cat ./ci_files/enforcer-config.json |\ jq '.px_app_id="${{ secrets.PX_APP_ID }}"' |\ jq '.px_cookie_secret="${{ secrets.TEST_COOKIE_SECRET }}"' |\ - jq '.px_auth_token="${{ secrets.PX_AUTH_TOKEN }}"' > /tmp/enforcer-config.json + jq '.px_auth_token="${{ secrets.PX_AUTH_TOKEN }}"' |\ + jq '.px_logger_auth_token="${{ secrets.PX_LOGGER_AUTH_TOKEN }}"' > /tmp/enforcer-config.json - name: log enforcer config run: cat /tmp/enforcer-config.json @@ -122,6 +125,9 @@ jobs: -f ./ci_files/enforcer-values.yaml \ --set image.name=localhost:5001/java-enforcer-sample-site \ --set image.tag=$SAMPLE_SITE_IMAGE_TAG \ + --set appId=${{ secrets.PX_APP_ID }} \ + --set cookieSecret=${{ secrets.TEST_COOKIE_SECRET }} \ + --set authToken=${{ secrets.PX_AUTH_TOKEN }} \ --set-file enforcerConfig.content=/tmp/enforcer-config.json \ --wait diff --git a/.gitignore b/.gitignore index d6827f60..de33b11b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ examples/examples.iml .DS_Store .classpath .project -.factorypath \ No newline at end of file +.factorypath +.smarttomcat \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 809ba2ce..a1112973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,19 @@ # Change Log +## [v6.16.0](https://github.com/PerimeterX/perimeterx-java-sdk/compare/6.16.0...HEAD) (2025-11-12) +- Added support for data enrichment header feature (`px_data_enrichment_header_name` configuration) +- Added support for AD user identifiers feature +- Added `px_secured_pxhd_enabled` configuration option to enable secure flag on `pxhd` cookie +- Added `is_sensitive_route` to risk api and async activities +- Added `additional_token_info` to risk api and async activities +- Updated telemetry activity to new format (`static_config` and `active_config`; `remote_config` is not supported) +- Updated telemetry activity to include `request_id` +- Updated captcha page template to newest version +- Updated dependencies minor and patch versions (major versions unchanged) +- Changed custom parameters to be of type `Object` instead of `String` to allow more flexibility +- Changed first party block script in captcha template to end with expected `/captcha.js` +- Changed `RequestWrapper` to include custom headers in methods that retrieve request headers +- Fixed possible connection leak issue due to unclosed responses in first party and telemetry requests +- Fixed first party fuzzing errors by returning 400 on first party requests with URL length > 1000 characters ## [v6.15.1](https://github.com/PerimeterX/perimeterx-java-sdk/compare/6.15.1...HEAD) (2025-09-08) - Added additional updateReason RISK to Telemetry flow diff --git a/CONFIGURATIONS.md b/CONFIGURATIONS.md index d6ba5f63..5d64eed9 100644 --- a/CONFIGURATIONS.md +++ b/CONFIGURATIONS.md @@ -53,6 +53,14 @@ Directives |loginResponseValidationStatusCode|Array of status codes that is used to validate if the login was successful.|{200}|int[] |customLoginResponseValidator|Custom class that validates if the login was successful. LoginResponseValidator must be implemented to be able to use this class.|DefaultCustomLoginResponseValidator|LoginResponseValidator |credentialsCustomExtractor|Custom class that extracts the login credentials. CredentialsExtractor must be implemented to be able to use this class.|DefaultCredentialsCustomExtractor|CredentialsExtractor +||pxDataEnrichmentHeaderName|Header name for forwarding data enrichment payload to origin server. When set, the SDK will add the PXDE payload as a header that can be forwarded to backend services.|"" (empty string)|String|Used with data enrichment feature +||securedPxhdEnabled|Enable secure flag on pxhd cookie for enhanced security in HTTPS-only environments.|false|boolean| +||pxJwtCookieName|Name of the cookie containing JWT token for user identifier extraction.|null|String|Part of Account Defender JWT user identifiers feature +||pxJwtCookieUserIdFieldName|Field name in JWT payload to extract as user ID from cookie.|null|String|Supports dot notation for nested fields (e.g., "user.id") +||pxJwtCookieAdditionalFieldNames|List of additional field names to extract from JWT cookie payload.|Empty List|List|Supports dot notation for nested fields +||pxJwtHeaderName|Name of the header containing JWT token for user identifier extraction.|null|String|Part of Account Defender JWT user identifiers feature +||pxJwtHeaderUserIdFieldName|Field name in JWT payload to extract as user ID from header.|null|String|Supports dot notation for nested fields (e.g., "sub") +||pxJwtHeaderAdditionalFieldNames|List of additional field names to extract from JWT header payload.|Empty List|List|Supports dot notation for nested fields ## Interfaces `perimeterx-java-sdk` can be tuned and set a different type of interface in order to make the module more flexible diff --git a/README.md b/README.md index 431e3c02..134a856f 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # [PerimeterX](http://www.perimeterx.com) Java SDK -> Latest stable version: [v6.15.0](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.15.0%7Cjar) +> Latest stable version: [v6.16.0](https://search.maven.org/#artifactdetails%7Ccom.perimeterx%7Cperimeterx-sdk%7C6.16.0%7Cjar) ## Table of Contents @@ -152,8 +152,12 @@ Please continue reading about the various configurations available on the sdk in #### Data Enrichment - pxde(PerimeterX Data Enrichment) -Users can use the additional activity handler to retrieve information for the request using the pxde object. -First, check that the data enrichment object is verified, then you can access it's properties. +Users can access data enrichment information in two ways: + +1. **Using context.getPxde()** - Access the data enrichment payload directly in your Java code +2. **Using a custom header** - Forward the data enrichment payload as a header to another server (e.g., your origin server) + +##### Accessing Data Enrichment in Java Code MyVerificationHandler.java: ```java @@ -191,6 +195,26 @@ enforcer.setVerificationHandler(new MyVerificationHandler(config)); ... ``` +##### Forwarding Data Enrichment as a Header + +To forward the data enrichment payload to your backend/origin server, configure the header name. After `pxVerify` completes, the PXDE payload will be automatically added as a header to the request, which can then be forwarded: + +```java +PXConfiguration config = new PXConfiguration.Builder() + ... + .pxDataEnrichmentHeaderName("X-PX-Data-Enrichment") + .build(); +PerimeterX enforcer = new PerimeterX(config); + +// In your filter: +PXContext ctx = enforcer.pxVerify(request, response); + +// After pxVerify, the request now contains the data enrichment header +// and can be forwarded to your backend/origin server +// The header will be available as "X-PX-Data-Enrichment" in the request +filterChain.doFilter(request, response); +``` + #### Custom Sensitive Request With the `customIsSensitive` predicate you can force the request to be sensitive. The input of the function is the same request that sent to the method `pxVerify`. @@ -220,6 +244,8 @@ The input of the function is the same request that sent to the method `pxVerify` If the function throws exception, it is equivalent to returning empty custom params. Implementing this configuration overrides the deprecated configuration `customParameterProvider`. +Custom parameters support various types including strings, numbers, and booleans, allowing flexibility in the data sent to PerimeterX. + > **Note** > The request body can only be read once by default. If your function requires reading the body > consider using RequestWrapper which caches the body. Send the wrapped request to @@ -234,12 +260,58 @@ PXConfiguration pxConfiguration = new PXConfiguration.Builder() CustomParameters customParameters = new CustomParameters(); customParameters.setCustomParam1("example-value"); customParameters.setCustomParam2(req.getHeader("example-header")); + customParameters.setCustomParam3(123); // Numbers are supported + customParameters.setCustomParam4(true); // Booleans are supported return customParameters; }) .build(); ... ``` +#### JWT User Identifiers (Account Defender) + +The SDK can extract user identifiers from JWT tokens in cookies or headers to enhance Account Defender capabilities. This allows PerimeterX to correlate user activity across sessions and improve detection accuracy. + +Configure JWT extraction from cookies: +```java +PXConfiguration pxConfiguration = new PXConfiguration.Builder() + ... + .pxJwtCookieName("authCookie") + .pxJwtCookieUserIdFieldName("userId") + .pxJwtCookieAdditionalFieldNames(Arrays.asList("email", "role")) + .build(); +``` + +Configure JWT extraction from headers: +```java +PXConfiguration pxConfiguration = new PXConfiguration.Builder() + ... + .pxJwtHeaderName("Authorization") + .pxJwtHeaderUserIdFieldName("sub") + .pxJwtHeaderAdditionalFieldNames(Arrays.asList("exp", "iss")) + .build(); +``` + +The SDK will: +1. First attempt to extract user identifiers from the configured cookie +2. If not found, attempt to extract from the configured header +3. Support dot notation for nested fields (e.g., "user.id") +4. Automatically handle Bearer token prefixes in headers + +#### Secured PXHD Cookie + +For enhanced security in HTTPS-only environments, you can enable the secure flag on the `pxhd` cookie. This ensures the cookie is only transmitted over secure connections: + +```java +PXConfiguration pxConfiguration = new PXConfiguration.Builder() + ... + .securedPxhdEnabled(true) + .build(); +``` + +> **Note** +> Only enable this in environments where all traffic is served over HTTPS, as the cookie will not be sent over HTTP connections when this flag is enabled. + #### Multiple Application Support Simply create multiple instances of the PerimeterX class: ```java diff --git a/ci_files/enforcer-config.json b/ci_files/enforcer-config.json index 6f3ed977..a8d5a097 100644 --- a/ci_files/enforcer-config.json +++ b/ci_files/enforcer-config.json @@ -19,9 +19,6 @@ "px_custom_first_party_xhr_endpoint": "/custom_first_party_xhr_endpoint", "px_custom_first_party_captcha_endpoint": "/custom_first_party_captcha_endpoint", "px_custom_first_party_prefix": "/custom_first_party_prefix", - "px_filter_by_route": [ - "/filtered_route" - ], "px_monitored_routes": [ "/monitored_route", "/monitored_route/suffix", @@ -116,5 +113,7 @@ ], "px_cors_support_enabled": true, "px_cors_preflight_request_filter_enabled": true, - "px_url_decode_reserved_characters": true + "px_url_decode_reserved_characters": true, + "px_secured_pxhd_enabled": true, + "px_data_enrichment_header_name": "X-PX-Data-Enrichment" } \ No newline at end of file diff --git a/ci_files/spec-tests-values.yaml b/ci_files/spec-tests-values.yaml index 2b025ef9..ec158e28 100644 --- a/ci_files/spec-tests-values.yaml +++ b/ci_files/spec-tests-values.yaml @@ -1,3 +1,9 @@ +additionalArgs: + - "--retries" + - "3" + - "--retry-delay" + - "10" + internalMockCollectorURL: "http://mock-collector-mock-collector:3001" siteURL: "http://java-enforcer-sample-site:3000" diff --git a/pom.xml b/pom.xml index e32fe360..cb429817 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ PerimeterX JAVA SDK com.perimeterx perimeterx-sdk - 6.15.1 + 6.16.0 jar PerimeterX Java SDK @@ -132,19 +132,19 @@ com.fasterxml.jackson.core jackson-databind - 2.11.4 + 2.17.2 com.fasterxml.jackson.core jackson-annotations - 2.10.2 + 2.17.2 commons-io commons-io - 2.8.0 + 2.14.0 @@ -156,7 +156,7 @@ org.apache.commons commons-lang3 - 3.12.0 + 3.18.0 @@ -168,19 +168,19 @@ org.slf4j slf4j-api - 1.7.30 + 1.7.36 org.apache.httpcomponents httpasyncclient - 4.1.4 + 4.1.5 org.apache.httpcomponents httpclient - 4.5.13 + 4.5.14 @@ -207,14 +207,14 @@ org.springframework spring-test - 4.3.1.RELEASE + 4.3.30.RELEASE test org.springframework spring-web - 4.3.1.RELEASE + 4.3.30.RELEASE test @@ -228,7 +228,7 @@ com.google.code.gson gson - 2.8.6 + 2.10.1 compile diff --git a/px_metadata.json b/px_metadata.json index d4dacd1d..107c7889 100644 --- a/px_metadata.json +++ b/px_metadata.json @@ -1,44 +1,47 @@ { - "version": "6.15.1", + "version": "6.16.0", "supported_features": [ "advanced_blocking_response", - "bypass_monitor_header", + "batched_activities", "block_activity", "block_page_captcha", + "block_page_hard_block", "block_page_js_challenge", "block_page_rate_limit", + "bypass_monitor_header", "client_ip_extraction", "cookie_v3", "credentials_intelligence", "css_ref", + "custom_cookie_header", "custom_logo", "custom_parameters", "custom_proxy", "custom_sensitive_request", + "data_enrichment_header", "enforced_routes", - "logger", + "enforcer_error", "filter_by_extension", "first_party", + "header_based_logger", "js_ref", + "logger", "mobile_support", "module_enable", "module_mode", "monitored_routes", "page_requested_activity", "pxde", - "vid_extraction", + "pxhd", "risk_api", - "custom_cookie_header", + "sensitive_headers", "sensitive_routes", "telemetry_command", - "enforcer_error", - "pxhd", - "batched_activities", - "sensitive_headers", - "block_page_hard_block", - "header_based_logger" + "user_identifiers", + "vid_extraction" ], "excluded_tests": [ + "test_block_activity_ad_block", "test_pxde_extraction_(s2s|unverified|verified)", "test_risk_cookie_valid_cookie_with_user_agent_of_max_length", "test_path_parsing_in_(block|page_requested|risk_api|additional_s2s)\\[with_dots", @@ -48,6 +51,9 @@ "test_block_page_captcha_response_contains_http_reason_phrase", "test_cookie_v3_cookie_decryption_failed_iterations_above_max_allowed_value", "test_cookie_v3_cookie_validation_failed_big_cookie", - "test_client_ip_extraction_order_risk_api" + "test_client_ip_extraction_order_risk_api", + "test_telemetry_command_verify_custom_function_hash", + "test_first_party_timeout", + "test_huge_jwt_(cookie|header)_with_user_id_and_additional_fields" ] } diff --git a/src/main/java/com/perimeterx/api/PerimeterX.java b/src/main/java/com/perimeterx/api/PerimeterX.java index 1813b53f..42d7bd85 100644 --- a/src/main/java/com/perimeterx/api/PerimeterX.java +++ b/src/main/java/com/perimeterx/api/PerimeterX.java @@ -58,18 +58,17 @@ import com.perimeterx.utils.logger.IPXLogger; import com.perimeterx.utils.StringUtils; import com.perimeterx.utils.logger.LoggerFactory; -import edu.emory.mathcs.backport.java.util.Collections; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponseWrapper; import java.io.Closeable; import java.io.IOException; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; -import java.util.List; import static com.perimeterx.utils.Constants.*; import static com.perimeterx.utils.PXCommonUtils.cookieKeysToCheck; @@ -240,6 +239,7 @@ private void addCustomHeadersToRequest(HttpServletRequest request, PXContext con setBreachedAccount(request, context); setAdditionalS2SActivityHeaders(request, context); } + setDataEnrichmentHeader(request, context); } private void setBreachedAccount(HttpServletRequest request, PXContext context) { @@ -260,6 +260,26 @@ private void setAdditionalS2SActivityHeaders(HttpServletRequest request, PXConte } } + private void setDataEnrichmentHeader(HttpServletRequest request, PXContext context) { + try { + String headerName = configuration.getPxDataEnrichmentHeaderName(); + if (headerName == null || headerName.isEmpty()) { + return; + } + + if (context.getPxde() == null || !context.isPxdeVerified()) { + return; + } + + 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); + } catch (Exception e) { + context.logger.debug("Failed to add data enrichment header", e); + } + } + public void pxPostVerify(ResponseWrapper response, PXContext context) throws PXException { try { if (context != null) { diff --git a/src/main/java/com/perimeterx/api/activities/BufferedActivityHandler.java b/src/main/java/com/perimeterx/api/activities/BufferedActivityHandler.java index fb6c006e..d704f37c 100644 --- a/src/main/java/com/perimeterx/api/activities/BufferedActivityHandler.java +++ b/src/main/java/com/perimeterx/api/activities/BufferedActivityHandler.java @@ -57,7 +57,7 @@ public void handlePageRequestedActivity(PXContext context) throws PXException { @Override public void handleEnforcerTelemetryActivity(PXConfiguration pxConfig, UpdateReason updateReason, PXContext context) { try { - EnforcerTelemetryActivityDetails details = new EnforcerTelemetryActivityDetails(pxConfig, updateReason); + EnforcerTelemetryActivityDetails details = new EnforcerTelemetryActivityDetails(pxConfig, context, updateReason); EnforcerTelemetry enforcerTelemetry = new EnforcerTelemetry("enforcer_telemetry", pxConfig.getAppId(), details); this.client.sendEnforcerTelemetry(enforcerTelemetry, context); } catch (IOException e) { diff --git a/src/main/java/com/perimeterx/api/activities/DefaultActivityHandler.java b/src/main/java/com/perimeterx/api/activities/DefaultActivityHandler.java index 045341d2..53bd4d0b 100644 --- a/src/main/java/com/perimeterx/api/activities/DefaultActivityHandler.java +++ b/src/main/java/com/perimeterx/api/activities/DefaultActivityHandler.java @@ -48,7 +48,7 @@ public void handlePageRequestedActivity(PXContext context) throws PXException { @Override public void handleEnforcerTelemetryActivity(PXConfiguration pxConfiguration, UpdateReason updateReason, PXContext context) { try { - EnforcerTelemetryActivityDetails details = new EnforcerTelemetryActivityDetails(pxConfiguration, updateReason); + EnforcerTelemetryActivityDetails details = new EnforcerTelemetryActivityDetails(pxConfiguration, context, updateReason); EnforcerTelemetry enforcerTelemetry = new EnforcerTelemetry("enforcer_telemetry", pxConfiguration.getAppId(), details); this.client.sendEnforcerTelemetry(enforcerTelemetry, context); } catch (Exception e) { diff --git a/src/main/java/com/perimeterx/api/blockhandler/templates/TemplateFactory.java b/src/main/java/com/perimeterx/api/blockhandler/templates/TemplateFactory.java index 998aa3cf..e626ee97 100644 --- a/src/main/java/com/perimeterx/api/blockhandler/templates/TemplateFactory.java +++ b/src/main/java/com/perimeterx/api/blockhandler/templates/TemplateFactory.java @@ -61,7 +61,7 @@ public static Map getProps(PXContext pxContext, PXConfiguration String hostUrl = pxContext.getCollectorURL(); if (pxConfig.isFirstPartyEnabled() && !pxContext.isMobileToken()) { String prefix = pxConfig.getAppId().substring(2); - blockScript = SLASH + prefix + Constants.FIRST_PARTY_CAPTCHA_PATH + QUESTION_MARK + captchaSrcParams; + blockScript = SLASH + prefix + Constants.FIRST_PARTY_CAPTCHA_PATH + CAPTCHA_FIRST_PARTY_FILE_PATH + QUESTION_MARK + captchaSrcParams; jsClientSrc = SLASH + prefix + Constants.FIRST_PARTY_VENDOR_PATH; hostUrl = SLASH + prefix + Constants.FIRST_PARTY_XHR_PATH; } diff --git a/src/main/java/com/perimeterx/api/proxy/DefaultReverseProxy.java b/src/main/java/com/perimeterx/api/proxy/DefaultReverseProxy.java index 3158ca86..04ee6728 100644 --- a/src/main/java/com/perimeterx/api/proxy/DefaultReverseProxy.java +++ b/src/main/java/com/perimeterx/api/proxy/DefaultReverseProxy.java @@ -106,8 +106,8 @@ public boolean reversePxXhr(HttpServletRequest req, HttpServletResponse res, PXC final String url = pxConfiguration.getCollectorUrl() + path; final String host = pxConfiguration.getCollectorUrl().replaceFirst("https?:\\/\\/", ""); - if (!isValidThirdPartyUrl(url, host, path)) { - context.logger.error("First party XHR URL is inaccurate: " + url + ", rendering default response"); + if (!isValidThirdPartyUrl(url, host, path, context)) { + context.logger.error("first party XHR URL is inaccurate: " + url + ", rendering default response"); predefinedResponseHelper.handlePredefinedResponse(res, predefinedResponse, context); return true; } @@ -134,13 +134,26 @@ private String getPath(HttpServletRequest req) { return isBlank(req.getRequestURI()) ? "" : req.getRequestURI().substring(xhrPrefix.length()); } - private boolean isValidThirdPartyUrl(String rawThirdPartyUrl, String expectedHost, String expectedUrl) { + private boolean isValidThirdPartyUrl(String rawThirdPartyUrl, String expectedHost, String expectedUrl, PXContext context) { try { URL url = new URL(rawThirdPartyUrl); String uri = url.getPath() + (url.getQuery() != null ? url.getQuery() : ""); - return url.getHost().equalsIgnoreCase(expectedHost) && uri.startsWith(expectedUrl); + if (!uri.startsWith(expectedUrl)) { + context.logger.debug("Validating third party URL failed, expected URL does not match the request URL. expectedUrl: " + expectedUrl + ", requestUrl: " + uri); + return false; + } + String host = url.getHost(); + final int port = url.getPort(); + if (port != -1 && port != url.getDefaultPort()) { + host += ":" + port; + } + if (!host.equalsIgnoreCase(expectedHost)) { + context.logger.debug("Validating third party URL failed, expected host does not match the request host. expectedHost: " + expectedHost + ", requestHost: " + host); + return false; + } + return true; } catch (Exception e) { - PerimeterX.globalLogger.error("Failed to parse rawUrl. ", e.getMessage()); + context.logger.error("Failed to parse rawUrl. ", e.getMessage()); } return false; diff --git a/src/main/java/com/perimeterx/api/proxy/RemoteServer.java b/src/main/java/com/perimeterx/api/proxy/RemoteServer.java index dafc32d2..44f810fb 100644 --- a/src/main/java/com/perimeterx/api/proxy/RemoteServer.java +++ b/src/main/java/com/perimeterx/api/proxy/RemoteServer.java @@ -109,9 +109,12 @@ public IPXOutgoingRequest prepareProxyRequest() throws IOException { return requestBuilder.build(); } - public IPXIncomingResponse handleResponse(IPXOutgoingRequest proxyRequest, PXContext context) { + public void handleResponse(IPXOutgoingRequest proxyRequest, PXContext context) { IPXIncomingResponse proxyResponse = null; try { + if (proxyRequest != null && proxyRequest.getUrl().length() > maxUrlLength) { + throw new IllegalArgumentException("URL too long: " + proxyRequest.getUrl().length()); + } // Execute the request proxyResponse = doExecute(proxyRequest); int statusCode = proxyResponse.status().getStatusCode(); @@ -119,7 +122,7 @@ public IPXIncomingResponse handleResponse(IPXOutgoingRequest proxyRequest, PXCon // In failure we can check if we enable predefined request or proxy the original response if (this.isAllowedPredefinedResponse() && statusCode >= HttpStatus.SC_BAD_REQUEST) { predefinedResponseHelper.handlePredefinedResponse(res, predefinedResponse, context); - return proxyResponse; + return; } res.setStatus(statusCode); @@ -139,12 +142,35 @@ public IPXIncomingResponse handleResponse(IPXOutgoingRequest proxyRequest, PXCon copyResponseEntity(proxyResponse); } + } catch (IllegalArgumentException e) { + context.logger.debug("Invalid request in first-party proxy: {}", e.getMessage()); + handleClientError(context); } catch (Exception e) { if (this.isAllowedPredefinedResponse()) { predefinedResponseHelper.handlePredefinedResponse(res, predefinedResponse, context); } + } finally { + if (proxyResponse != null) { + try { + proxyResponse.close(); + } catch (IOException e) { + context.logger.debug("Failed to close proxy response", e); + } + } + } + } + + private void handleClientError(PXContext context) { + try { + res.setStatus(HttpStatus.SC_BAD_REQUEST); + res.setContentType("text/plain"); + res.setCharacterEncoding("UTF-8"); + res.getWriter().print("Bad Request"); + res.getWriter().flush(); + } catch (IOException e) { + context.logger.error("Failed to write error response: {}", e.getMessage()); + res.setStatus(HttpStatus.SC_BAD_REQUEST); } - return proxyResponse; } /** diff --git a/src/main/java/com/perimeterx/api/verificationhandler/DefaultVerificationHandler.java b/src/main/java/com/perimeterx/api/verificationhandler/DefaultVerificationHandler.java index 716dcfee..cdb193f3 100644 --- a/src/main/java/com/perimeterx/api/verificationhandler/DefaultVerificationHandler.java +++ b/src/main/java/com/perimeterx/api/verificationhandler/DefaultVerificationHandler.java @@ -1,13 +1,11 @@ package com.perimeterx.api.verificationhandler; -import com.perimeterx.api.PerimeterX; import com.perimeterx.api.activities.ActivityHandler; import com.perimeterx.api.additionalContext.PXHDSource; import com.perimeterx.api.blockhandler.BlockHandler; import com.perimeterx.models.PXContext; import com.perimeterx.models.configuration.PXConfiguration; import com.perimeterx.models.exceptions.PXException; -import com.perimeterx.utils.logger.IPXLogger; import com.perimeterx.utils.logger.LogReason; import javax.servlet.http.HttpServletResponseWrapper; @@ -96,6 +94,10 @@ private String getPxhdCookie(PXContext context) throws UnsupportedEncodingExcept cookieValue += COOKIE_SEPARATOR + COOKIE_DOMAIN_KEY + context.getPxhdDomain(); } + if (pxConfiguration.isSecuredPxhdEnabled()) { + cookieValue += COOKIE_SEPARATOR + "Secure"; + } + return cookieValue; } diff --git a/src/main/java/com/perimeterx/http/PXHttpClient.java b/src/main/java/com/perimeterx/http/PXHttpClient.java index 93993591..6680fc11 100644 --- a/src/main/java/com/perimeterx/http/PXHttpClient.java +++ b/src/main/java/com/perimeterx/http/PXHttpClient.java @@ -217,14 +217,27 @@ public PXDynamicConfiguration getConfigurationFromServer() { @Override public void sendEnforcerTelemetry(EnforcerTelemetry enforcerTelemetry, PXContext context) throws IOException { - String requestBody = JsonUtils.writer.writeValueAsString(enforcerTelemetry); - if (context!=null){ - context.logger.debug("Sending enforcer telemetry: {}", requestBody); - } else{ - logger.debug("Sending enforcer telemetry: {}", requestBody); + IPXIncomingResponse httpResponse = null; + try { + String requestBody = JsonUtils.writer.writeValueAsString(enforcerTelemetry); + if (context != null) { + context.logger.debug("Sending enforcer telemetry: {}", requestBody); + } else { + logger.debug("Sending enforcer telemetry: {}", requestBody); + } + IPXOutgoingRequest request = buildOutgoingRequest(this.pxConfiguration.getServerURL() + Constants.API_ENFORCER_TELEMETRY, PXHttpMethod.POST, requestBody); + httpResponse = client.send(request); + } catch (Exception e) { + if (context != null) { + context.logger.debug("Sending enforcer telemetry failed. Error: {}", e.getMessage()); + } else { + logger.debug("Sending enforcer telemetry failed. Error: {}", e.getMessage()); + } + } finally { + if (httpResponse != null) { + httpResponse.close(); + } } - IPXOutgoingRequest request = buildOutgoingRequest(this.pxConfiguration.getServerURL() + Constants.API_ENFORCER_TELEMETRY,PXHttpMethod.POST, requestBody); - client.send(request); } private IPXOutgoingRequest buildOutgoingRequest(String url , PXHttpMethod method, String requestBody, BasicHeader... headers) { diff --git a/src/main/java/com/perimeterx/http/RequestWrapper.java b/src/main/java/com/perimeterx/http/RequestWrapper.java index 84e29844..448130b2 100644 --- a/src/main/java/com/perimeterx/http/RequestWrapper.java +++ b/src/main/java/com/perimeterx/http/RequestWrapper.java @@ -7,8 +7,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStreamReader; -import java.util.HashMap; -import java.util.Map; +import java.util.*; /** * Reading HttpServletRequest is limited to one time only @@ -25,6 +24,12 @@ public RequestWrapper(HttpServletRequest request) { this.customHeaders = new HashMap<>(); } + // Add a custom header to the request + public void addHeader(String name, String value) { + this.customHeaders.put(name, value); + } + + // Modify body methods to read from the cached body @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(getBody().getBytes()); @@ -36,6 +41,7 @@ public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } + // Modify header methods to include custom headers @Override public String getHeader(String name) { String headerValue = customHeaders.get(name); @@ -43,11 +49,52 @@ public String getHeader(String name) { if (headerValue != null) { return headerValue; } - return ((HttpServletRequest) getRequest()).getHeader(name); + return super.getHeader(name); } - public void addHeader(String name, String value) { - this.customHeaders.put(name, value); + @Override + public Enumeration getHeaderNames() { + Enumeration headerNames = super.getHeaderNames(); + List list = Collections.list(headerNames); + for (String customHeaderName : customHeaders.keySet()) { + if (!list.contains(customHeaderName)) { + list.add(customHeaderName); + } + } + return Collections.enumeration(list); + } + + @Override + public Enumeration getHeaders(String name) { + String headerValue = customHeaders.get(name); + if (headerValue != null) { + List list = new ArrayList<>(); + list.add(headerValue); + return Collections.enumeration(list); + } + return super.getHeaders(name); + } + + @Override + public int getIntHeader(String name) throws NumberFormatException { + final String headerValue = getHeader(name); + if (headerValue != null) { + return Integer.parseInt(headerValue); + } + return -1; + } + + @Override + public long getDateHeader(String name) throws IllegalArgumentException { + final String headerValue = getHeader(name); + if (headerValue != null) { + try { + return Long.parseLong(headerValue); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Header " + name + " is not a valid date"); + } + } + return -1L; } public synchronized String getBody() throws IOException { diff --git a/src/main/java/com/perimeterx/internals/JwtUserIdentifiersExtractor.java b/src/main/java/com/perimeterx/internals/JwtUserIdentifiersExtractor.java new file mode 100644 index 00000000..a1f8b545 --- /dev/null +++ b/src/main/java/com/perimeterx/internals/JwtUserIdentifiersExtractor.java @@ -0,0 +1,105 @@ +package com.perimeterx.internals; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.perimeterx.models.PXContext; +import com.perimeterx.models.configuration.PXConfiguration; + +import javax.servlet.http.Cookie; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.*; + +public final class JwtUserIdentifiersExtractor { + private static final ObjectMapper OM = new ObjectMapper(); + + private JwtUserIdentifiersExtractor() {} + + public static void attachJwtIfConfigured(PXContext ctx, PXConfiguration cfg) { + tryExtractFromCookie(ctx, cfg); + if (ctx.getJwtAppUserId() != null || (ctx.getJwtAdditionalFields() != null && !ctx.getJwtAdditionalFields().isEmpty())) { + return; + } + tryExtractFromHeader(ctx, cfg); + } + + private static void tryExtractFromCookie(PXContext ctx, PXConfiguration cfg) { + String cookieName = nullToEmpty(cfg.getPxJwtCookieName()); + if (cookieName.isEmpty()) return; + String token = findCookieValue(ctx, cookieName); + buildAndSet(ctx, token, cfg.getPxJwtCookieUserIdFieldName(), safeList(cfg.getPxJwtCookieAdditionalFieldNames())); + } + + private static void tryExtractFromHeader(PXContext ctx, PXConfiguration cfg) { + String headerName = nullToEmpty(cfg.getPxJwtHeaderName()); + if (headerName.isEmpty()) return; + String raw = ctx.getRequest().getHeader(headerName); + if (raw == null) return; + String token = raw.startsWith("Bearer ") ? raw.substring(7).trim() : raw.trim(); + buildAndSet(ctx, token, cfg.getPxJwtHeaderUserIdFieldName(), safeList(cfg.getPxJwtHeaderAdditionalFieldNames())); + } + + private static void buildAndSet(PXContext ctx, String token, String userPath, List additionalPaths) { + if (isEmpty(token)) return; + try { + Map payload = decodePayload(token); + String appUserId = asString(getByDotPath(payload, userPath)); + Map additional = collectByDotPaths(payload, additionalPaths); + if (isEmpty(appUserId) && additional.isEmpty()) return; + ctx.setJwtAppUserId(appUserId); + ctx.setJwtAdditionalFields(additional.isEmpty() ? null : additional); + } catch (Exception e) { + ctx.logger.debug("JWT extraction skipped: invalid token", e); + } + } + + private static Map decodePayload(String jwt) throws Exception { + String[] parts = jwt.split("\\."); + if (parts.length < 2) throw new IllegalArgumentException("Invalid JWT"); + byte[] json = Base64.getUrlDecoder().decode(parts[1]); + return OM.readValue(json, Map.class); + } + + private static String findCookieValue(PXContext ctx, String name) { + Cookie[] cookies = ctx.getRequest().getCookies(); + if (cookies == null) return null; + for (Cookie c : cookies) { + if (name.equals(c.getName())) { + String v = c.getValue(); + try { + return v != null ? URLDecoder.decode(v, StandardCharsets.UTF_8.toString()) : null; + } catch (UnsupportedEncodingException e) { + return v; + } + } + } + return null; + } + + private static Object getByDotPath(Map json, String path) { + if (json == null || isEmpty(path)) return null; + Object cur = json; + for (String seg : path.split("\\.")) { + if (!(cur instanceof Map)) return null; + cur = ((Map) cur).get(seg); + if (cur == null) return null; + } + return cur; + } + + private static Map collectByDotPaths(Map json, List paths) { + Map out = new LinkedHashMap<>(); + for (String p : paths) { + Object v = getByDotPath(json, p); + if (v != null) out.put(p, v); + } + return out; + } + + private static List safeList(List l) { return l == null ? Collections.emptyList() : l; } + private static boolean isEmpty(String s) { return s == null || s.isEmpty(); } + private static String nullToEmpty(String s) { return s == null ? "" : s; } + private static String asString(Object v) { return v == null ? null : String.valueOf(v); } +} + + diff --git a/src/main/java/com/perimeterx/internals/PXS2SValidator.java b/src/main/java/com/perimeterx/internals/PXS2SValidator.java index 1c6ce83e..5822f448 100644 --- a/src/main/java/com/perimeterx/internals/PXS2SValidator.java +++ b/src/main/java/com/perimeterx/internals/PXS2SValidator.java @@ -1,9 +1,7 @@ package com.perimeterx.internals; -import com.perimeterx.api.PerimeterX; import com.perimeterx.api.additionalContext.PXHDSource; import com.perimeterx.http.PXClient; -import com.perimeterx.internals.cookie.DataEnrichmentCookie; import com.perimeterx.models.PXContext; import com.perimeterx.models.configuration.PXConfiguration; import com.perimeterx.models.exceptions.PXException; @@ -14,7 +12,6 @@ import com.perimeterx.models.risk.S2SErrorReasonInfo; import com.perimeterx.utils.Constants; import com.perimeterx.utils.EnforcerErrorUtils; -import com.perimeterx.utils.logger.IPXLogger; import com.perimeterx.utils.logger.LogReason; import org.apache.http.conn.ConnectTimeoutException; @@ -99,9 +96,10 @@ private void updateContextFromResponse(PXContext pxContext, RiskResponse respons pxContext.setRiskScore(response.getScore()); pxContext.setUuid(response.getUuid()); pxContext.setBlockAction(response.getAction()); - DataEnrichmentCookie dataEnrichment = new DataEnrichmentCookie(response.getDataEnrichment(), true); - pxContext.setPxde(dataEnrichment.getJsonPayload()); - pxContext.setPxdeVerified(dataEnrichment.isValid()); + if (response.getDataEnrichment() != null) { + pxContext.setPxde(response.getDataEnrichment()); + pxContext.setPxdeVerified(true); + } if(isNoneBlank(response.getPxhd())) { pxContext.setPxhd(response.getPxhd()); diff --git a/src/main/java/com/perimeterx/internals/cookie/AbstractPXCookie.java b/src/main/java/com/perimeterx/internals/cookie/AbstractPXCookie.java index d4c40bb8..0899761a 100644 --- a/src/main/java/com/perimeterx/internals/cookie/AbstractPXCookie.java +++ b/src/main/java/com/perimeterx/internals/cookie/AbstractPXCookie.java @@ -186,4 +186,9 @@ public String getUUID() { public String getVID() { return decodedCookie.get("v").asText(); } + + @Override + public String additionalTokenInfo() { + return this.decodedCookie.get("add") != null ? this.decodedCookie.get("add").asText() : null; + } } diff --git a/src/main/java/com/perimeterx/internals/cookie/PXCookie.java b/src/main/java/com/perimeterx/internals/cookie/PXCookie.java index 84d36b5f..3b977af9 100644 --- a/src/main/java/com/perimeterx/internals/cookie/PXCookie.java +++ b/src/main/java/com/perimeterx/internals/cookie/PXCookie.java @@ -23,4 +23,5 @@ public interface PXCookie { boolean isSecured() throws PXException; + String additionalTokenInfo(); } diff --git a/src/main/java/com/perimeterx/models/PXContext.java b/src/main/java/com/perimeterx/models/PXContext.java index 90b7c38c..cab4b02a 100644 --- a/src/main/java/com/perimeterx/models/PXContext.java +++ b/src/main/java/com/perimeterx/models/PXContext.java @@ -13,6 +13,7 @@ import com.perimeterx.internals.cookie.cookieparsers.CookieHeaderParser; import com.perimeterx.internals.cookie.cookieparsers.HeaderParser; import com.perimeterx.internals.cookie.cookieparsers.MobileCookieHeaderParser; +import com.perimeterx.internals.JwtUserIdentifiersExtractor; import com.perimeterx.models.configuration.ModuleMode; import com.perimeterx.models.configuration.PXConfiguration; import com.perimeterx.models.enforcererror.EnforcerErrorReasonInfo; @@ -232,6 +233,12 @@ public class PXContext { private String pxhdDomain; private String pxCtsCookie; private long enforcerStartTime; + private boolean isSensitiveRequest; + private String additionalTokenInfo; + + // JWT user identifiers + private String jwtAppUserId; + private Map jwtAdditionalFields; /** * The cookie key used to decrypt the cookie @@ -289,8 +296,9 @@ private void postInitContext(final HttpServletRequest request, PXConfiguration p this.enforcerErrorReasonInfo = new EnforcerErrorReasonInfo(); this.sensitiveHeaders = pxConfiguration.getSensitiveHeaders(); - String protocolDetails[] = request.getProtocol().split("/"); + String[] protocolDetails = request.getProtocol().split("/"); this.httpVersion = protocolDetails.length > 1 ? protocolDetails[1] : StringUtils.EMPTY; + this.isSensitiveRequest = determineIsSensitiveRequest(); CustomParametersProvider customParametersProvider = pxConfiguration.getCustomParametersProvider(); Function customParametersExtraction = pxConfiguration.getCustomParametersExtraction(); @@ -303,6 +311,12 @@ private void postInitContext(final HttpServletRequest request, PXConfiguration p } catch (Exception e) { logger.debug("failed to extract custom parameters from custom function", e); } + + try { + JwtUserIdentifiersExtractor.attachJwtIfConfigured(this, pxConfiguration); + } catch (Exception e) { + logger.debug("jwt identifiers extraction failed", e); + } } private IPXLogger getLogger(){ @@ -310,7 +324,12 @@ private IPXLogger getLogger(){ boolean isLoggerHeaderRequest = requestLoggerAuthToken!=null && this.getPxConfiguration().getLoggerAuthToken().equals(requestLoggerAuthToken); return pxConfiguration.getLoggerFactory().getRequestContextLogger(isLoggerHeaderRequest); } + public boolean isSensitiveRequest() { + return this.isSensitiveRequest; + } + + private boolean determineIsSensitiveRequest() { return this.isContainCredentialsIntelligence() || checkSensitiveRoute(pxConfiguration.getSensitiveRoutes(), servletPath) || checkSensitiveRouteRegex(pxConfiguration.getSensitiveRoutesRegex(), servletPath) @@ -453,6 +472,7 @@ public void setOriginalTokenCookie(String originalTokenCookie) { public void setRiskCookie(AbstractPXCookie riskCookie) { this.riskCookie = riskCookie.getDecodedCookie().toString(); + this.additionalTokenInfo = riskCookie.additionalTokenInfo(); } public void setBlockAction(String blockAction) { diff --git a/src/main/java/com/perimeterx/models/activities/CommonActivityDetails.java b/src/main/java/com/perimeterx/models/activities/CommonActivityDetails.java index c5baf184..8fbde006 100644 --- a/src/main/java/com/perimeterx/models/activities/CommonActivityDetails.java +++ b/src/main/java/com/perimeterx/models/activities/CommonActivityDetails.java @@ -9,6 +9,7 @@ import com.perimeterx.models.httpmodels.Additional; import lombok.Getter; +import java.util.Map; import java.util.UUID; @Getter @@ -50,6 +51,9 @@ public class CommonActivityDetails implements ActivityDetails { @JsonProperty("additional_risk_info") public String additionalRiskInfo; + @JsonProperty("additional_token_info") + public String additionalTokenInfo; + @JsonProperty("user") public String username; @@ -59,6 +63,15 @@ public class CommonActivityDetails implements ActivityDetails { @JsonProperty("cross_tab_session") public String pxCtsCookie; + @JsonProperty("app_user_id") + public String appUserId; + + @JsonProperty("jwt_additional_fields") + public Map jwtAdditionalFields; + + @JsonProperty("is_sensitive_route") + public Boolean isSensitiveRoute; + public CommonActivityDetails(PXContext context) { final LoginData loginData = context.getLoginData(); @@ -85,6 +98,9 @@ public CommonActivityDetails(PXContext context) { this.riskStartTime = additional.riskStartTime; this.enforcerStartTime = additional.enforcerStartTime; this.pxCtsCookie = additional.pxCtsCookie; - + this.appUserId = additional.appUserId; + this.jwtAdditionalFields = additional.jwtAdditionalFields; + this.isSensitiveRoute = additional.isSensitiveRoute; + this.additionalTokenInfo = additional.additionalTokenInfo; } } diff --git a/src/main/java/com/perimeterx/models/activities/EnforcerTelemetryActivityDetails.java b/src/main/java/com/perimeterx/models/activities/EnforcerTelemetryActivityDetails.java index 3117a07e..fdda722e 100644 --- a/src/main/java/com/perimeterx/models/activities/EnforcerTelemetryActivityDetails.java +++ b/src/main/java/com/perimeterx/models/activities/EnforcerTelemetryActivityDetails.java @@ -1,13 +1,14 @@ package com.perimeterx.models.activities; import com.fasterxml.jackson.annotation.JsonProperty; -import com.google.gson.Gson; import com.google.gson.JsonIOException; +import com.perimeterx.models.PXContext; import com.perimeterx.models.configuration.PXConfiguration; import com.perimeterx.utils.Constants; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.UUID; /** * Created by nitzangoldfeder on 29/10/2017. @@ -16,19 +17,27 @@ public class EnforcerTelemetryActivityDetails implements ActivityDetails { @JsonProperty("module_version") private String moduleVersion; + @JsonProperty("enforcer_configs") - private String enforcerConfigs; + private TelemetryConfiguration enforcerConfigs; + @JsonProperty("os_name") private String osName; + @JsonProperty("node_name") private String nodeName; + @JsonProperty("update_reason") private UpdateReason updateReason; - public EnforcerTelemetryActivityDetails(PXConfiguration pxConfiguration, UpdateReason updateReason) { + @JsonProperty("request_id") + private UUID requestId; + + public EnforcerTelemetryActivityDetails(PXConfiguration pxConfiguration, PXContext context, UpdateReason updateReason) { this.moduleVersion = Constants.SDK_VERSION; this.osName = System.getProperty("os.name"); this.updateReason = updateReason; + this.requestId = context.getRequestId(); try { this.nodeName = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { @@ -36,11 +45,13 @@ public EnforcerTelemetryActivityDetails(PXConfiguration pxConfiguration, UpdateR } try { - Gson gson = new Gson(); - String pxConfigJson = gson.toJson(pxConfiguration.getTelemetryConfig()); - this.enforcerConfigs = pxConfigJson; + PXConfiguration config = pxConfiguration.getTelemetryConfig(); + enforcerConfigs = new TelemetryConfiguration(); + enforcerConfigs.activeConfig = config; + enforcerConfigs.staticConfig = config; + enforcerConfigs.remoteConfig = null; // remote config not supported } catch (JsonIOException e) { - this.enforcerConfigs = "Could not retrieve pxConfiguration"; + enforcerConfigs = null; } } @@ -48,7 +59,7 @@ public String getModuleVersion() { return moduleVersion; } - public String getEnforcerConfigs() { + public TelemetryConfiguration getEnforcerConfigs() { return enforcerConfigs; } @@ -59,4 +70,19 @@ public String getOsName() { public String getNodeName() { return nodeName; } + + public UUID getRequestId() { + return requestId; + } +} + +class TelemetryConfiguration { + @JsonProperty("active_config") + public PXConfiguration activeConfig; + + @JsonProperty("static_config") + public PXConfiguration staticConfig; + + @JsonProperty("remote_config") + public PXConfiguration remoteConfig; } diff --git a/src/main/java/com/perimeterx/models/configuration/ModuleMode.java b/src/main/java/com/perimeterx/models/configuration/ModuleMode.java index ef121579..901b0761 100644 --- a/src/main/java/com/perimeterx/models/configuration/ModuleMode.java +++ b/src/main/java/com/perimeterx/models/configuration/ModuleMode.java @@ -10,13 +10,11 @@ * Created by nitzangoldfeder on 26/06/2017. */ public enum ModuleMode { - MONITOR(0), BLOCKING(1); - private int value; - - private static Map namesMap = new HashMap<>(2); + private final int value; + private static final Map namesMap = new HashMap<>(2); static { namesMap.put(0, MONITOR); namesMap.put(1, BLOCKING); @@ -27,26 +25,12 @@ public static ModuleMode forValue(Integer value) { return namesMap.get(value); } - @JsonValue - public Integer toValue() { - for (Map.Entry entry : namesMap.entrySet()) { - if (entry.getValue() == this) - return entry.getKey(); - } - return 0; - } - - ModuleMode(int value) { - this.value = value; - } - @JsonValue public int getValue() { return this.value; } - @JsonCreator - public void setValue(int value) { + ModuleMode(int value) { this.value = value; } } diff --git a/src/main/java/com/perimeterx/models/configuration/PXConfiguration.java b/src/main/java/com/perimeterx/models/configuration/PXConfiguration.java index 1f0665d1..ca3ca7f2 100644 --- a/src/main/java/com/perimeterx/models/configuration/PXConfiguration.java +++ b/src/main/java/com/perimeterx/models/configuration/PXConfiguration.java @@ -1,6 +1,7 @@ package com.perimeterx.models.configuration; import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.perimeterx.api.PerimeterX; import com.perimeterx.api.additionalContext.credentialsIntelligence.CIProtocol; @@ -48,6 +49,27 @@ @AllArgsConstructor @NoArgsConstructor @Getter +@JsonIgnoreProperties({ + "customParametersProvider", + "blockHandler", + "customLoginResponseValidator", + "credentialsCustomExtractor", + "customIsSensitiveRequest", + "customParametersExtraction", + "filterByCustomFunction", + "loggerFactory", + "telemetryConfig", + "reverseProxyInstance", + "ipxHttpClientInstance", + "ipxhttpClientInstance", + "IPXHttpClientInstance", + "pxClientInstance", + "PXClientInstance", + "pxclientInstance", + "httpClient", + "pxClient", + "pxReverseProxy" +}) public class PXConfiguration { private static LoggerSeverity loggerSeverity = null; @@ -256,6 +278,10 @@ public static void setPxLoggerSeverity(LoggerSeverity severity) { @JsonProperty("px_login_successful_status") private int[] loginResponseValidationStatusCode = {200}; + @Builder.Default + @JsonProperty("px_secured_pxhd_enabled") + private boolean securedPxhdEnabled = false; + @Builder.Default private LoginResponseValidator customLoginResponseValidator = new DefaultCustomLoginResponseValidator(); @@ -308,11 +334,79 @@ public static void setPxLoggerSeverity(LoggerSeverity severity) { @Builder.Default private Predicate filterByCustomFunction = req -> false; + + // --- JWT user identifiers configuration --- + @JsonProperty("px_jwt_cookie_name") + private String pxJwtCookieName; + + @JsonProperty("px_jwt_cookie_user_id_field_name") + private String pxJwtCookieUserIdFieldName; + + @Builder.Default + @JsonProperty("px_jwt_cookie_additional_field_names") + private List pxJwtCookieAdditionalFieldNames = new ArrayList<>(); + + @JsonProperty("px_jwt_header_name") + private String pxJwtHeaderName; + + @JsonProperty("px_jwt_header_user_id_field_name") + private String pxJwtHeaderUserIdFieldName; + + @Builder.Default + @JsonProperty("px_jwt_header_additional_field_names") + private List pxJwtHeaderAdditionalFieldNames = new ArrayList<>(); + + @Builder.Default + @JsonProperty("px_data_enrichment_header_name") + private String pxDataEnrichmentHeaderName = ""; + /** * @return Configuration Object clone without cookieKey and authToken **/ public PXConfiguration getTelemetryConfig() { - return this.toBuilder().clearCookieKeys().authToken(null).build(); + + PXConfiguration telemetry = this.toBuilder() + .authToken(this.redactString(this.authToken)) + .clearCookieKeys() + .cookieKeys(this.cookieKeys.stream().map(this::redactString).collect(Collectors.toList())) + .loggerAuthToken(this.redactString(this.loggerAuthToken)) + .sensitiveRoutesRegex(this.stringifyRegexSet(this.sensitiveRoutesRegex)) + // prune non-serializable/runtime members for telemetry clone + .customParametersProvider(null) + .blockHandler(null) + .customLoginResponseValidator(null) + .credentialsCustomExtractor(null) + .customIsSensitiveRequest(null) + .customParametersExtraction(null) + .filterByCustomFunction(null) + .loggerFactory(null) + .httpClient(null) + .pxClient(null) + .pxReverseProxy(null) + .build(); + // ensure transient instances are not serialized + telemetry.pxClientInstance = null; + telemetry.ipxHttpClientInstance = null; + telemetry.reverseProxyInstance = null; + return telemetry; + } + + private Set stringifyRegexSet(Set regexSet) { + if (regexSet == null) { + return null; + } + return regexSet.stream() + .map(r -> r != null && r.startsWith("_REGEXP ") ? r : "_REGEXP /" + r + "/") + .collect(Collectors.toSet()); + } + + private String redactString(String str) { + int trailingChars = 5; + String redactedPrefix = "***REDACTED***"; + if (str == null || str.length() <= trailingChars) { + return redactedPrefix; + } + return redactedPrefix.concat(str.substring(str.length() - trailingChars)); } public void disableModule() { diff --git a/src/main/java/com/perimeterx/models/httpmodels/Additional.java b/src/main/java/com/perimeterx/models/httpmodels/Additional.java index 2c4df51c..aebce120 100644 --- a/src/main/java/com/perimeterx/models/httpmodels/Additional.java +++ b/src/main/java/com/perimeterx/models/httpmodels/Additional.java @@ -12,6 +12,7 @@ import com.perimeterx.utils.Constants; import java.util.Date; +import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -85,14 +86,28 @@ public class Additional { @JsonProperty("request_id") public UUID requestId; + @JsonProperty("enforcer_start_time") public long enforcerStartTime; + @JsonProperty("risk_start_time") public long riskStartTime; @JsonProperty("cross_tab_session") public String pxCtsCookie; + @JsonProperty("app_user_id") + public String appUserId; + + @JsonProperty("jwt_additional_fields") + public Map jwtAdditionalFields; + + @JsonProperty("is_sensitive_route") + public Boolean isSensitiveRoute; + + @JsonProperty("additional_token_info") + public String additionalTokenInfo; + public static Additional fromContext(PXContext ctx) { Additional additional = new Additional(); additional.pxCookie = ctx.getRiskCookie(); @@ -114,6 +129,10 @@ public static Additional fromContext(PXContext ctx) { additional.enforcerStartTime = ctx.getEnforcerStartTime(); additional.riskStartTime = new Date().getTime(); additional.pxCtsCookie = ctx.getPxCtsCookie(); + additional.appUserId = ctx.getJwtAppUserId(); + additional.jwtAdditionalFields = ctx.getJwtAdditionalFields(); + additional.isSensitiveRoute = ctx.isSensitiveRequest(); + additional.additionalTokenInfo = ctx.getAdditionalTokenInfo(); setLoginCredentials(ctx, additional); diff --git a/src/main/java/com/perimeterx/models/risk/CustomParameters.java b/src/main/java/com/perimeterx/models/risk/CustomParameters.java index 6a51cb9c..34e4c993 100644 --- a/src/main/java/com/perimeterx/models/risk/CustomParameters.java +++ b/src/main/java/com/perimeterx/models/risk/CustomParameters.java @@ -6,30 +6,32 @@ /** * Created by nitzangoldfeder on 03/04/2018. */ + +// Note: Parameters are of type Object to allow flexibility in the type of data being sent (e.g., String, Number, Boolean, etc.) @JsonInclude(JsonInclude.Include.NON_NULL) public class CustomParameters { @JsonProperty("custom_param1") - public String customParam1; + public Object customParam1; @JsonProperty("custom_param2") - public String customParam2; + public Object customParam2; @JsonProperty("custom_param3") - public String customParam3; + public Object customParam3; @JsonProperty("custom_param4") - public String customParam4; + public Object customParam4; @JsonProperty("custom_param5") - public String customParam5; + public Object customParam5; @JsonProperty("custom_param6") - public String customParam6; + public Object customParam6; @JsonProperty("custom_param7") - public String customParam7; + public Object customParam7; @JsonProperty("custom_param8") - public String customParam8; + public Object customParam8; @JsonProperty("custom_param9") - public String customParam9; + public Object customParam9; @JsonProperty("custom_param10") - public String customParam10; + public Object customParam10; - public String getCustomParam1() { + public Object getCustomParam1() { return customParam1; } @@ -37,7 +39,7 @@ public void setCustomParam1(String customParam1) { this.customParam1 = customParam1; } - public String getCustomParam2() { + public Object getCustomParam2() { return customParam2; } @@ -45,7 +47,7 @@ public void setCustomParam2(String customParam2) { this.customParam2 = customParam2; } - public String getCustomParam3() { + public Object getCustomParam3() { return customParam3; } @@ -53,7 +55,7 @@ public void setCustomParam3(String customParam3) { this.customParam3 = customParam3; } - public String getCustomParam4() { + public Object getCustomParam4() { return customParam4; } @@ -61,7 +63,7 @@ public void setCustomParam4(String customParam4) { this.customParam4 = customParam4; } - public String getCustomParam5() { + public Object getCustomParam5() { return customParam5; } @@ -69,7 +71,7 @@ public void setCustomParam5(String customParam5) { this.customParam5 = customParam5; } - public String getCustomParam6() { + public Object getCustomParam6() { return customParam6; } @@ -77,7 +79,7 @@ public void setCustomParam6(String customParam6) { this.customParam6 = customParam6; } - public String getCustomParam7() { + public Object getCustomParam7() { return customParam7; } @@ -85,7 +87,7 @@ public void setCustomParam7(String customParam7) { this.customParam7 = customParam7; } - public String getCustomParam8() { + public Object getCustomParam8() { return customParam8; } @@ -93,7 +95,7 @@ public void setCustomParam8(String customParam8) { this.customParam8 = customParam8; } - public String getCustomParam9() { + public Object getCustomParam9() { return customParam9; } @@ -101,7 +103,7 @@ public void setCustomParam9(String customParam9) { this.customParam9 = customParam9; } - public String getCustomParam10() { + public Object getCustomParam10() { return customParam10; } diff --git a/src/main/java/com/perimeterx/utils/logger/LoggerFactory.java b/src/main/java/com/perimeterx/utils/logger/LoggerFactory.java index 42b400c3..26ed9eb3 100644 --- a/src/main/java/com/perimeterx/utils/logger/LoggerFactory.java +++ b/src/main/java/com/perimeterx/utils/logger/LoggerFactory.java @@ -9,7 +9,7 @@ public IPXLogger getRequestContextLogger(boolean isMemoryEnabled) { if (pxLoggerSeverity == null) { return new Slf4JLogger(isMemoryEnabled); } else { - return new ConsoleLogger(pxLoggerSeverity,isMemoryEnabled); + return new ConsoleLogger(pxLoggerSeverity, isMemoryEnabled); } } public IPXLogger getRequestContextLogger() { diff --git a/src/main/resources/com/perimeterx/api/blockhandler/templates/captcha_template.mustache b/src/main/resources/com/perimeterx/api/blockhandler/templates/captcha_template.mustache index 4a1503bf..bd4aade7 100644 --- a/src/main/resources/com/perimeterx/api/blockhandler/templates/captcha_template.mustache +++ b/src/main/resources/com/perimeterx/api/blockhandler/templates/captcha_template.mustache @@ -10,70 +10,71 @@ {{/cssRef}} - -{{#jsRef}} - -{{/jsRef}} + function isContentLoaded() { + return !!document.querySelector('div,span'); + } + window._pxOnError = function () { + var style = document.createElement('style'); + style.innerText = '@import url(https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap);body{background-color:#fafbfc}.px-captcha-error-container{position:fixed;height:340px;background-color:#fff;font-family:Roboto,sans-serif}.px-captcha-error-header{color:#f0f1f2;font-size:29px;margin:67px 0 33px;font-weight:500;line-height:.83;text-align:center}.px-captcha-error-message{color:#f0f1f2;font-size:18px;margin:0 0 29px;line-height:1.33;text-align:center}.px-captcha-error-button{text-align:center;line-height:48px;width:253px;margin:auto;border-radius:50px;border:solid 1px #f0f1f2;font-size:20px;color:#f0f1f2}.px-captcha-error-wrapper{margin:18px 0 0}div.px-captcha-error{margin:auto;text-align:center;width:400px;height:30px;font-size:12px;background-color:#fcf0f2;color:#ce0e2d}img.px-captcha-error{margin:6px 8px -2px 0}.px-captcha-error-refid{border-top:solid 1px #f0eeee;height:27px;margin:13px 0 0;border-radius:0 0 3px 3px;background-color:#fafbfc;font-size:10px;line-height:2.5;text-align:center;color:#b1b5b8}@media (min-width:620px){.px-captcha-error-container{width:530px;top:50%;left:50%;margin-top:-170px;margin-left:-265px;border-radius:3px;box-shadow:0 2px 9px -1px rgba(0,0,0,.13)}}@media (min-width:481px) and (max-width:620px){.px-captcha-error-container{width:85%;top:50%;left:50%;margin-top:-170px;margin-left:-42.5%;border-radius:3px;box-shadow:0 2px 9px -1px rgba(0,0,0,.13)}}@media (max-width:480px){body{background-color:#fff}.px-captcha-error-header{color:#f0f1f2;font-size:29px;margin:55px 0 33px}.px-captcha-error-container{width:530px;top:50%;left:50%;margin-top:-170px;margin-left:-265px}.px-captcha-error-refid{position:fixed;width:100%;left:0;bottom:0;border-radius:0;font-size:14px;line-height:2}}@media (max-width:390px){div.px-captcha-error{font-size:10px}.px-captcha-error-refid{font-size:11px;line-height:2.5}}'; + document.head.appendChild(style); + var div = document.createElement('div'); + div.className = 'px-captcha-error-container'; + div.innerHTML = '
Before we continue...
Press & Hold to confirm you are
a human (and not a bot).
Press & Hold
Please check your internet connection' + (window._pxMobile ? '' : ' or disable your ad-blocker') + '.
Reference ID ' + window._pxUuid + '
'; + document.body.appendChild(div); + if (window._pxMobile) { + setTimeout(function() { + location.href = '/px/captcha_close?status=-1'; + }, 5000); + } + }; + + {{#jsRef}} + + {{/jsRef}} diff --git a/src/test/java/com/perimeterx/api/RequestWrapperTest.java b/src/test/java/com/perimeterx/api/RequestWrapperTest.java index 4f2a122b..ca0a5ba2 100644 --- a/src/test/java/com/perimeterx/api/RequestWrapperTest.java +++ b/src/test/java/com/perimeterx/api/RequestWrapperTest.java @@ -6,6 +6,8 @@ import java.io.BufferedReader; import java.io.IOException; +import java.util.Collections; +import java.util.List; import static org.testng.Assert.*; @@ -64,4 +66,95 @@ public void testSpecialCharacters() throws IOException { RequestWrapper requestWrapper = new RequestWrapper(req); assertEquals(requestWrapper.getBody(), new String(bytes)); } + + @Test + public void testGetHeader() { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.addHeader("header1", "value1"); + RequestWrapper requestWrapper = new RequestWrapper(req); + requestWrapper.addHeader("header2", "value2"); + + assertEquals(requestWrapper.getHeader("header1"), "value1"); + assertEquals(requestWrapper.getHeader("header2"), "value2"); + } + + @Test + public void testGetHeaderNames() { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.addHeader("header1", "value1"); + RequestWrapper requestWrapper = new RequestWrapper(req); + requestWrapper.addHeader("header2", "value2"); + + boolean foundHeader1 = false; + boolean foundHeader2 = false; + for (String headerName : Collections.list(requestWrapper.getHeaderNames())) { + if (headerName.equals("header1")) { + foundHeader1 = true; + } + if (headerName.equals("header2")) { + foundHeader2 = true; + } + } + assertTrue(foundHeader1); + assertTrue(foundHeader2); + } + + @Test + public void testGetHeaders() { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.addHeader("header1", "value1"); + RequestWrapper requestWrapper = new RequestWrapper(req); + requestWrapper.addHeader("header2", "value2"); + + List header1Values = Collections.list(requestWrapper.getHeaders("header1")); + assertEquals(header1Values.size(), 1); + for (String headerValue : header1Values) { + assertEquals(headerValue, "value1"); + } + + List header2Values = Collections.list(requestWrapper.getHeaders("header2")); + assertEquals(header2Values.size(), 1); + for (String headerValue : header2Values) { + assertEquals(headerValue, "value2"); + } + } + + @Test + public void testGetIntHeader() { + MockHttpServletRequest req = new MockHttpServletRequest(); + req.addHeader("intHeader", "123"); + RequestWrapper requestWrapper = new RequestWrapper(req); + requestWrapper.addHeader("customIntHeader", "456"); + requestWrapper.addHeader("stringHeader", "stringValue"); + + assertEquals(requestWrapper.getIntHeader("intHeader"), 123); + assertEquals(requestWrapper.getIntHeader("customIntHeader"), 456); + assertEquals(requestWrapper.getIntHeader("nonExistentHeader"), -1); + try { + requestWrapper.getIntHeader("stringHeader"); + fail("Expected NumberFormatException"); + } catch (NumberFormatException e) { + // Expected exception + } + } + + @Test + public void testGetDateHeader() { + MockHttpServletRequest req = new MockHttpServletRequest(); + long now = System.currentTimeMillis(); + req.addHeader("dateHeader", Long.toString(now)); + RequestWrapper requestWrapper = new RequestWrapper(req); + requestWrapper.addHeader("customDateHeader", Long.toString(now + 1000)); + requestWrapper.addHeader("stringHeader", "stringValue"); + + assertEquals(requestWrapper.getDateHeader("dateHeader"), now); + assertEquals(requestWrapper.getDateHeader("customDateHeader"), now + 1000); + assertEquals(requestWrapper.getDateHeader("nonExistentHeader"), -1); + try { + requestWrapper.getDateHeader("stringHeader"); + fail("Expected IllegalArgumentException"); + } catch (IllegalArgumentException e) { + // Expected exception + } + } } diff --git a/src/test/java/com/perimeterx/models/PXContextTest.java b/src/test/java/com/perimeterx/models/PXContextTest.java index 36eef547..fe2705c1 100644 --- a/src/test/java/com/perimeterx/models/PXContextTest.java +++ b/src/test/java/com/perimeterx/models/PXContextTest.java @@ -5,6 +5,7 @@ import com.perimeterx.api.providers.HostnameProvider; import com.perimeterx.api.providers.IPProvider; import com.perimeterx.api.providers.RemoteAddressIPProvider; +import com.perimeterx.http.RequestWrapper; import com.perimeterx.models.configuration.PXConfiguration; import com.perimeterx.models.risk.CustomParameters; import org.mockito.Mockito; @@ -14,6 +15,7 @@ import org.testng.annotations.Test; import javax.servlet.http.HttpServletRequest; +import java.util.Collections; /** * Test {@link PXContext} @@ -53,4 +55,41 @@ public void customParamsTest() { Mockito.verify(spyTestCustomParamProvider).buildCustomParameters(pxConfig, context); } + + @Test + public void allRequestHeadersShouldBeInPXContext() { + CustomParameters customParameters = new CustomParameters(); + customParameters.setCustomParam1("number1"); + TestCustomParamProvider spyTestCustomParamProvider = Mockito.spy(new TestCustomParamProvider(customParameters)); + PXConfiguration pxConfig = PXConfiguration.builder() + .appId("APP_ID") + .authToken("AUTH_123") + .cookieKey("COOKIE_123") + .customParametersProvider(spyTestCustomParamProvider) + .build(); + ((MockHttpServletRequest) request).addHeader("TEST-BYPASS", "0"); + PXContext context = new PXContext(request, this.ipProvider, this.hostnameProvider, pxConfig); + Assert.assertEquals(context.getHeaders().size(), Collections.list(request.getHeaderNames()).size()); + } + + @Test + public void allRequestWrapperHeadersShouldBeInPXContext() { + CustomParameters customParameters = new CustomParameters(); + customParameters.setCustomParam1("number1"); + TestCustomParamProvider spyTestCustomParamProvider = Mockito.spy(new TestCustomParamProvider(customParameters)); + PXConfiguration pxConfig = PXConfiguration.builder() + .appId("APP_ID") + .authToken("AUTH_123") + .cookieKey("COOKIE_123") + .customParametersProvider(spyTestCustomParamProvider) + .build(); + ((MockHttpServletRequest) request).addHeader("TEST-BYPASS", "0"); + RequestWrapper requestWrapper = new RequestWrapper(request); + requestWrapper.addHeader("client-ip", "127.0.0.1"); + requestWrapper.addHeader("accept", "application/json"); + requestWrapper.addHeader("content-type", "application/json"); + + PXContext context = new PXContext(requestWrapper, this.ipProvider, this.hostnameProvider, pxConfig); + Assert.assertEquals(context.getHeaders().size(), Collections.list(request.getHeaderNames()).size() + 3); + } } diff --git a/web/pom.xml b/web/pom.xml index 404ffa14..6134691e 100644 --- a/web/pom.xml +++ b/web/pom.xml @@ -31,7 +31,7 @@ org.slf4j slf4j-api - 1.7.25 + 1.7.36 org.slf4j @@ -65,7 +65,7 @@ 8 8 - 6.15.1 + 6.16.0 diff --git a/web/src/main/java/com/web/Config.java b/web/src/main/java/com/web/Config.java index 1d7333f6..c00a994d 100644 --- a/web/src/main/java/com/web/Config.java +++ b/web/src/main/java/com/web/Config.java @@ -6,6 +6,7 @@ import com.perimeterx.models.configuration.PXConfiguration; import com.perimeterx.models.configuration.credentialsIntelligenceconfig.CILoginMap; import com.perimeterx.models.risk.CustomParameters; +import com.perimeterx.utils.logger.LoggerSeverity; import org.json.JSONArray; import org.json.JSONObject; @@ -153,6 +154,33 @@ public PXConfiguration getPxConfiguration() { case "px_login_successful_status": builder.loginResponseValidationStatusCode(extractStatusCode(key)); break; + case "px_logger_severity": + this.setLoggerSeverity(enforcerConfig.getString(key)); + break; + case "px_secured_pxhd_enabled": + builder.securedPxhdEnabled(enforcerConfig.getBoolean(key)); + break; + case "px_jwt_cookie_name": + builder.pxJwtCookieName(enforcerConfig.getString(key)); + break; + case "px_jwt_cookie_user_id_field_name": + builder.pxJwtCookieUserIdFieldName(enforcerConfig.getString(key)); + break; + case "px_jwt_cookie_additional_field_names": + builder.pxJwtCookieAdditionalFieldNames(extractStringList(key)); + break; + case "px_jwt_header_name": + builder.pxJwtHeaderName(enforcerConfig.getString(key)); + break; + case "px_jwt_header_user_id_field_name": + builder.pxJwtHeaderUserIdFieldName(enforcerConfig.getString(key)); + break; + case "px_jwt_header_additional_field_names": + builder.pxJwtHeaderAdditionalFieldNames(extractStringList(key)); + break; + case "px_data_enrichment_header_name": + builder.pxDataEnrichmentHeaderName(enforcerConfig.getString(key)); + break; case "px_user_agent_max_length": case "px_risk_cookie_max_length": case "px_risk_cookie_max_iterations": @@ -168,16 +196,35 @@ public PXConfiguration getPxConfiguration() { CustomParameters customParameters = new CustomParameters(); customParameters.customParam1 = "test1"; customParameters.customParam2 = "test2"; - customParameters.customParam3 = "3"; - customParameters.customParam4 = "4"; - customParameters.customParam5 = "5"; - customParameters.customParam6 = "6"; + customParameters.customParam3 = 3; + customParameters.customParam4 = 4; + customParameters.customParam5 = 5; + customParameters.customParam6 = 6; + customParameters.customParam7 = req.getRequestURI(); return customParameters; }); + builder.customIsSensitiveRequest((req -> { + return req.getRequestURI().startsWith("/sensitive") && req.getMethod().equals("POST"); + })); + return builder.build(); } + private void setLoggerSeverity(String severity) { + switch (severity) { + case "debug": + PXConfiguration.setPxLoggerSeverity(LoggerSeverity.DEBUG); + break; + case "error": + PXConfiguration.setPxLoggerSeverity(LoggerSeverity.ERROR); + break; + case "none": + PXConfiguration.setPxLoggerSeverity(LoggerSeverity.NONE); + break; + } + } + private int[] extractStatusCode(String key) { final JSONArray jsonField = enforcerConfig.getJSONArray(key); final int[] statusCode = new int[jsonField.length()]; @@ -187,5 +234,14 @@ private int[] extractStatusCode(String key) { } return statusCode; } + + private java.util.List extractStringList(String key) { + final JSONArray jsonField = enforcerConfig.getJSONArray(key); + final java.util.List out = new java.util.ArrayList<>(jsonField.length()); + for (int i = 0; i < jsonField.length(); i++) { + out.add(jsonField.getString(i)); + } + return out; + } } diff --git a/web/src/main/java/com/web/PXFilter.java b/web/src/main/java/com/web/PXFilter.java index 135140c7..d9dc6508 100644 --- a/web/src/main/java/com/web/PXFilter.java +++ b/web/src/main/java/com/web/PXFilter.java @@ -1,7 +1,6 @@ package com.web; import com.perimeterx.api.PerimeterX; -import com.perimeterx.api.additionalContext.credentialsIntelligence.loginrequest.CredentialsExtractorFactory; import com.perimeterx.http.RequestWrapper; import com.perimeterx.http.ResponseWrapper; import com.perimeterx.models.PXContext; @@ -39,6 +38,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha final PXContext context = pxFilter.pxVerify((HttpServletRequest) request, new HttpServletResponseWrapper((HttpServletResponse) response)); setDefaultPageAttributes((HttpServletRequest) request, config); + copyDataEnrichmentHeaderToResponse((HttpServletRequest) request, (HttpServletResponse) response); if (context != null && context.isRequestLowScore()) { filterChain.doFilter(request, response); @@ -46,7 +46,6 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha response = new ResponseWrapper((HttpServletResponse) response); pxFilter.pxPostVerify((ResponseWrapper) response, context); - } catch (PXException e) { filterChain.doFilter(request, response); } @@ -61,4 +60,16 @@ public void destroy() { e.printStackTrace(); } } + + private void copyDataEnrichmentHeaderToResponse(HttpServletRequest request, HttpServletResponse response) { + String dataEnrichmentHeaderName = config.getPxConfiguration().getPxDataEnrichmentHeaderName(); + if (dataEnrichmentHeaderName == null || dataEnrichmentHeaderName.isEmpty()) { + return; + } + + String dataEnrichmentHeaderValue = request.getHeader(dataEnrichmentHeaderName); + if (dataEnrichmentHeaderValue != null && !dataEnrichmentHeaderValue.isEmpty()) { + response.setHeader(dataEnrichmentHeaderName, dataEnrichmentHeaderValue); + } + } }