diff --git a/apiml-package/src/main/resources/bin/start.sh b/apiml-package/src/main/resources/bin/start.sh
index 957b50b85f..334939fa98 100755
--- a/apiml-package/src/main/resources/bin/start.sh
+++ b/apiml-package/src/main/resources/bin/start.sh
@@ -58,7 +58,6 @@
# - ZWE_configs_apiml_security_x509_certificatesUrl
# - ZWE_configs_apiml_security_x509_enabled
# - ZWE_configs_apiml_security_x509_registry_allowedUsers
-# - ZWE_configs_apiml_service_allowEncodedSlashes
# - ZWE_configs_apiml_service_corsEnabled
# - ZWE_configs_apiml_service_corsAllowedMethods
# - ZWE_configs_apiml_service_corsDefaultAllowedOrigins
@@ -317,7 +316,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${APIML_CODE} ${JAVA_BIN_DIR}java \
-Dapiml.security.x509.externalMapperUser=${ZWE_components_gateway_apiml_security_x509_externalMapperUser:-${ZWE_configs_apiml_security_x509_externalMapperUser:-${ZWE_zowe_setup_security_users_zowe:-ZWESVUSR}}} \
-Dapiml.security.x509.registry.allowedUsers=${ZWE_components_gateway_apiml_security_x509_registry_allowedUsers:-${ZWE_configs_apiml_security_x509_registry_allowedUsers:-}} \
-Dapiml.security.zosmf.applid=${ZWE_zosmf_applId:-IZUDFLT} \
- -Dapiml.service.allowEncodedSlashes=${ZWE_components_gateway_apiml_service_allowEncodedSlashes:-${ZWE_configs_apiml_service_allowEncodedSlashes:-true}} \
-Dapiml.service.apimlId=${ZWE_components_gateway_apimlId:-${ZWE_configs_apimlId:-}} \
-Dapiml.service.corsAllowedMethods=${ZWE_components_gateway_apiml_service_corsAllowedMethods:-${ZWE_configs_apiml_service_corsAllowedMethods:-GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS}} \
-Dapiml.service.corsEnabled=${ZWE_components_gateway_apiml_service_corsEnabled:-${ZWE_configs_apiml_service_corsEnabled:-false}} \
diff --git a/apiml-package/src/main/resources/schemas/apiml-config.json b/apiml-package/src/main/resources/schemas/apiml-config.json
index 5e231a129d..b90a25fec9 100644
--- a/apiml-package/src/main/resources/schemas/apiml-config.json
+++ b/apiml-package/src/main/resources/schemas/apiml-config.json
@@ -220,6 +220,11 @@
"description": "Enables direct native calls to z/OS to query distributed identity mappings and client certificate mappings. Use only if APIML is running on z/OS.",
"default": true
},
+ "enableStrictUrlValidation": {
+ "type": "boolean",
+ "description": "When enabled, the Gateway strictly validates request URLs and rejects encoded characters such as encoded slashes, backslashes, and semicolons. When disabled, validation is relaxed for routed traffic, while Gateway-internal endpoints remain strictly validated.",
+ "default": true
+ },
"auth": {
"type": "object",
"description": "Detail configuration of authentication schemes.",
@@ -550,11 +555,6 @@
"type": "object",
"description": "General configuration of the Gateway.",
"properties": {
- "allowEncodedSlashes": {
- "type": "boolean",
- "description": "When this parameter is set to true, the Gateway allows encoded characters to be part of URL requests redirected through the Gateway.",
- "default": true
- },
"corsEnabled": {
"type": "boolean",
"description": "Allow CORS on gateway.",
diff --git a/apiml/src/main/resources/application.yml b/apiml/src/main/resources/application.yml
index 5c5fe501da..ad8ffbd392 100644
--- a/apiml/src/main/resources/application.yml
+++ b/apiml/src/main/resources/application.yml
@@ -192,7 +192,6 @@ apiml:
apimlId: apiml1
corsEnabled: true
corsAllowedEndpoints: /gateway/**,/apicatalog/**,/cachingservice/**,/v3/api-docs/**
- allowEncodedSlashes: true
discoveryServiceUrls: https://localhost:10011/eureka/
forwardClientCertEnabled: false
hostname: localhost
diff --git a/apiml/src/test/java/org/zowe/apiml/acceptance/StrictUrlValidationTest.java b/apiml/src/test/java/org/zowe/apiml/acceptance/StrictUrlValidationTest.java
new file mode 100644
index 0000000000..22dfb36859
--- /dev/null
+++ b/apiml/src/test/java/org/zowe/apiml/acceptance/StrictUrlValidationTest.java
@@ -0,0 +1,158 @@
+/*
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Copyright Contributors to the Zowe Project.
+ */
+
+package org.zowe.apiml.acceptance;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Named;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.test.context.NestedTestConfiguration;
+import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.zowe.apiml.gateway.MockService;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import static io.restassured.RestAssured.given;
+import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+/**
+ * Modulith acceptance tests validating the {@code apiml.security.enableStrictUrlValidation} option. The two nested
+ * classes exercise the two states of the option, differentiated by {@link TestPropertySource}:
+ *
+ * - {@link WhenStrictValidationEnabled} - the default, strict URL validation rejects requests containing
+ * any of the encoded characters controlled by the option, whether the target is a routed service or an
+ * internal endpoint.
+ * - {@link WhenStrictValidationDisabled} - relaxed URL validation forwards such requests to the target
+ * service for routed traffic, but internal endpoints remain strictly validated.
+ *
+ * The parameterized cases cover every character relaxed by the option: encoded slash, encoded double slash,
+ * backslash, encoded percent, encoded period, and semicolon.
+ */
+@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)
+class StrictUrlValidationTest {
+
+ private static final String SERVICE_ID = "serviceid1";
+
+ /**
+ * Path suffixes, each containing one of the special characters controlled by
+ * {@code apiml.security.enableStrictUrlValidation}.
+ */
+ private static final List> SPECIAL_CHARACTERS = List.of(
+ Named.of("encoded slash", "encoded%2Fslash"),
+ Named.of("encoded double slash", "encoded%2F%2Fslash"),
+ Named.of("backslash", "encoded%5Cbackslash"),
+ Named.of("encoded percent", "encoded%25percent"),
+ Named.of("encoded period", "encoded%2Eperiod"),
+ Named.of("semicolon", "path;matrix")
+ );
+
+ /**
+ * Internal base paths, which are strictly validated even when the option is disabled. In the modulith the
+ * Gateway also fronts the API Catalog and Caching service, so this set is broader than in a standalone Gateway.
+ * Matches {@code BASE_PATHS_MODULITH} in {@code WebSecurity}.
+ */
+ private static final List INTERNAL_ENDPOINTS = List.of(
+ "/gateway", "/application", "/images", "/v3/api-docs", "/apicatalog", "/cachingservice"
+ );
+
+ static Stream specialCharacters() {
+ return SPECIAL_CHARACTERS.stream().map(Arguments::arguments);
+ }
+
+ static Stream internalEndpointsWithSpecialCharacters() {
+ return INTERNAL_ENDPOINTS.stream().flatMap(endpoint ->
+ SPECIAL_CHARACTERS.stream().map(character -> arguments(endpoint, character)));
+ }
+
+ @Nested
+ @AcceptanceTest
+ @TestPropertySource(properties = {
+ "apiml.security.enableStrictUrlValidation=true"
+ })
+ class WhenStrictValidationEnabled extends AcceptanceTestWithMockServices {
+
+ @BeforeAll
+ void setUp() {
+ mockService(SERVICE_ID).scope(MockService.Scope.CLASS)
+ .addEndpoint("/" + SERVICE_ID + "/test")
+ .and().start();
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.zowe.apiml.acceptance.StrictUrlValidationTest#specialCharacters")
+ void whenRoutedRequestContainsSpecialCharacter_thenRejectedWithBadRequest(String pathSuffix) {
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get(basePath + "/" + SERVICE_ID + "/api/v1/test/" + pathSuffix)
+ .then()
+ .statusCode(is(SC_BAD_REQUEST));
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.zowe.apiml.acceptance.StrictUrlValidationTest#internalEndpointsWithSpecialCharacters")
+ void whenInternalEndpointRequestContainsSpecialCharacter_thenRejectedWithBadRequest(String internalPath, String pathSuffix) {
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get(basePath + internalPath + "/" + pathSuffix)
+ .then()
+ .statusCode(is(SC_BAD_REQUEST));
+ }
+
+ }
+
+ @Nested
+ @AcceptanceTest
+ @TestPropertySource(properties = {
+ "apiml.security.enableStrictUrlValidation=false"
+ })
+ class WhenStrictValidationDisabled extends AcceptanceTestWithMockServices {
+
+ @BeforeAll
+ void setUp() {
+ mockService(SERVICE_ID).scope(MockService.Scope.CLASS)
+ .addEndpoint("/" + SERVICE_ID + "/test")
+ .and().start();
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.zowe.apiml.acceptance.StrictUrlValidationTest#specialCharacters")
+ void whenRoutedRequestContainsSpecialCharacter_thenForwardedToService(String pathSuffix) {
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get(basePath + "/" + SERVICE_ID + "/api/v1/test/" + pathSuffix)
+ .then()
+ .statusCode(is(SC_OK));
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.zowe.apiml.acceptance.StrictUrlValidationTest#internalEndpointsWithSpecialCharacters")
+ void whenInternalEndpointRequestContainsSpecialCharacter_thenStillRejectedWithBadRequest(String internalPath, String pathSuffix) {
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get(basePath + internalPath + "/" + pathSuffix)
+ .then()
+ .statusCode(is(SC_BAD_REQUEST));
+ }
+
+ }
+
+}
diff --git a/apiml/src/test/resources/application.yml b/apiml/src/test/resources/application.yml
index d4cccde2c8..1fa98ecdc1 100644
--- a/apiml/src/test/resources/application.yml
+++ b/apiml/src/test/resources/application.yml
@@ -131,7 +131,6 @@ apiml:
apimlId: apiml1
corsEnabled: true
corsAllowedEndpoints: /gateway/**,/apicatalog/**,/cachingservice/**,/v3/api-docs/**
- allowEncodedSlashes: true
discoveryServiceUrls: https://localhost:10011/eureka/
forwardClientCertEnabled: false
hostname: localhost
diff --git a/config/zowe-api-dev/alternative_start.sh b/config/zowe-api-dev/alternative_start.sh
index 9208ab1cba..58297d0c5c 100755
--- a/config/zowe-api-dev/alternative_start.sh
+++ b/config/zowe-api-dev/alternative_start.sh
@@ -58,7 +58,6 @@ _BPX_JOBNAME=${ZOWE_PREFIX}${GATEWAY_CODE} java \
-Dapiml.service.port=${GATEWAY_PORT} \
-Dapiml.service.discoveryServiceUrls=${ZWE_DISCOVERY_SERVICES_LIST} \
-Dapiml.service.preferIpAddress=${APIML_PREFER_IP_ADDRESS} \
- -Dapiml.service.allowEncodedSlashes=${APIML_ALLOW_ENCODED_SLASHES} \
-Dapiml.service.corsEnabled=${APIML_CORS_ENABLED} \
-Dapiml.catalog.serviceId=${APIML_GATEWAY_CATALOG_ID} \
-Dapiml.cache.storage.location=${WORKSPACE_DIR}/api-mediation/ \
diff --git a/config/zowe-api-dev/run-wrapper.sh b/config/zowe-api-dev/run-wrapper.sh
index ad1b2c6eac..59a73194f7 100755
--- a/config/zowe-api-dev/run-wrapper.sh
+++ b/config/zowe-api-dev/run-wrapper.sh
@@ -5,7 +5,6 @@ export PATH=$PATH:$JAVA_HOME/bin
# Variables required on shell:
# This list should be exhaustive, with variables that are not needed commented
# sorted alphabetically for easier maintenance where possible
-export APIML_ALLOW_ENCODED_SLASHES=true
export APIML_CORS_ENABLED=true
export APIML_DEBUG_MODE_ENABLED=true
export APIML_DIAG_MODE_ENABLED=false
diff --git a/gateway-package/src/main/resources/bin/start.sh b/gateway-package/src/main/resources/bin/start.sh
index ff5be9261f..6c879796b8 100755
--- a/gateway-package/src/main/resources/bin/start.sh
+++ b/gateway-package/src/main/resources/bin/start.sh
@@ -53,7 +53,6 @@
# - ZWE_configs_apiml_security_x509_acceptForwardedCert
# - ZWE_configs_apiml_security_x509_certificatesUrl
# - ZWE_configs_apiml_security_x509_registry_allowedUsers
-# - ZWE_configs_apiml_service_allowEncodedSlashes
# - ZWE_configs_apiml_service_corsEnabled
# - ZWE_configs_apiml_service_corsAllowedMethods
# - ZWE_configs_apiml_gateway_registry_enabled
@@ -194,7 +193,6 @@ _BPX_JOBNAME=${ZWE_zowe_job_prefix}${GATEWAY_CODE} ${JAVA_BIN_DIR}java \
-Dapiml.security.x509.certificatesUrls=${ZWE_configs_apiml_security_x509_certificatesUrls:-${ZWE_configs_apiml_security_x509_certificatesUrl:-}} \
-Dapiml.security.x509.enabled=${ZWE_configs_apiml_security_x509_enabled:-false} \
-Dapiml.security.x509.registry.allowedUsers=${ZWE_configs_apiml_security_x509_registry_allowedUsers:-} \
- -Dapiml.service.allowEncodedSlashes=${ZWE_configs_apiml_service_allowEncodedSlashes:-true} \
-Dapiml.service.apimlId=${ZWE_configs_apimlId:-} \
-Dapiml.service.corsAllowedMethods=${ZWE_configs_apiml_service_corsAllowedMethods:-GET,HEAD,POST,PATCH,DELETE,PUT,OPTIONS} \
-Dapiml.service.corsDefaultAllowedHeaders=${ZWE_configs_apiml_service_corsDefaultAllowedHeaders:-} \
diff --git a/gateway-package/src/main/resources/schemas/gateway-config.json b/gateway-package/src/main/resources/schemas/gateway-config.json
index ecaedfbeba..70b20ce9ec 100644
--- a/gateway-package/src/main/resources/schemas/gateway-config.json
+++ b/gateway-package/src/main/resources/schemas/gateway-config.json
@@ -149,6 +149,11 @@
"description": "Enables direct native calls to z/OS to query distributed identity mappings and client certificate mappings. Use only if APIML is running on z/OS.",
"default": true
},
+ "enableStrictUrlValidation": {
+ "type": "boolean",
+ "description": "When enabled, the Gateway strictly validates request URLs and rejects encoded characters such as encoded slashes, backslashes, and semicolons. When disabled, validation is relaxed for routed traffic, while Gateway-internal endpoints remain strictly validated.",
+ "default": true
+ },
"auth": {
"type": "object",
"description": "Detail configuration of authentication schemes.",
@@ -509,11 +514,6 @@
}
}
},
- "allowEncodedSlashes": {
- "type": "boolean",
- "description": "When this parameter is set to true, the Gateway allows encoded characters to be part of URL requests redirected through the Gateway.",
- "default": true
- },
"corsEnabled": {
"type": "boolean",
"description": "Allow CORS on gateway.",
diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RoutingConfig.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RoutingConfig.java
index 7f92ade75b..dffdba6541 100644
--- a/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RoutingConfig.java
+++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/config/RoutingConfig.java
@@ -28,8 +28,6 @@ public class RoutingConfig {
@Value("${apiml.security.x509.acceptForwardedCert:false}")
private boolean acceptForwardedCert;
- @Value("${apiml.service.allowEncodedSlashes:true}")
- private boolean allowEncodedSlashes;
@Bean
public List commonNoRetryFilters() {
@@ -41,12 +39,6 @@ public List commonNoRetryFilters() {
filters.add(acceptForwardedClientCertFilter);
}
- if (!allowEncodedSlashes) {
- var encodedSlashesFilter = new FilterDefinition();
- encodedSlashesFilter.setName("ForbidEncodedSlashesFilterFactory");
- filters.add(encodedSlashesFilter);
- }
-
var secureHeaders = new FilterDefinition();
secureHeaders.setName("SecureHeaders");
filters.add(secureHeaders);
diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandler.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandler.java
index 3871d3438a..c2eee77d4f 100644
--- a/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandler.java
+++ b/gateway-service/src/main/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandler.java
@@ -35,7 +35,6 @@
import org.zowe.apiml.exception.MetadataValidationException;
import org.zowe.apiml.gateway.config.InvalidForwardException;
import org.zowe.apiml.gateway.filters.ForbidCharacterException;
-import org.zowe.apiml.gateway.filters.ForbidSlashException;
import org.zowe.apiml.gateway.filters.ZaasInternalErrorException;
import org.zowe.apiml.message.core.Message;
import org.zowe.apiml.message.core.MessageService;
@@ -109,12 +108,6 @@ public Mono handleForbidCharacterException(ServerWebExchange exchange, For
return setBodyResponse(exchange, SC_BAD_REQUEST, "org.zowe.apiml.gateway.requestContainEncodedCharacter");
}
- @ExceptionHandler(ForbidSlashException.class)
- public Mono handleForbidSlashException(ServerWebExchange exchange, ForbidSlashException ex) {
- log.debug("Forbidden slash in the URI {}: {}", exchange.getRequest().getURI(), ex.getMessage());
- return setBodyResponse(exchange, SC_BAD_REQUEST, "org.zowe.apiml.gateway.requestContainEncodedSlash");
- }
-
@ExceptionHandler({AuthenticationException.class, WebClientResponseException.Unauthorized.class})
public Mono handleAuthenticationException(ServerWebExchange exchange, Exception ex) {
log.debug("Unauthorized access on {}: {}", exchange.getRequest().getURI(), ex.getMessage());
diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ForbidEncodedSlashesFilterFactory.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ForbidEncodedSlashesFilterFactory.java
deleted file mode 100644
index f3a406b539..0000000000
--- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ForbidEncodedSlashesFilterFactory.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * This program and the accompanying materials are made available under the terms of the
- * Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- * Copyright Contributors to the Zowe Project.
- */
-
-package org.zowe.apiml.gateway.filters;
-
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.stereotype.Component;
-
-/**
- * This filter checks if encoded slashes in the URI are allowed based on configuration.
- * If not allowed and encoded slashes are present, it returns a BAD_REQUEST response.
- */
-@Component
-@ConditionalOnProperty(name = "apiml.service.allowEncodedSlashes", havingValue = "false", matchIfMissing = true)
-public class ForbidEncodedSlashesFilterFactory extends AbstractEncodedCharactersFilterFactory {
-
- private static final String ENCODED_SLASH = "%2f";
-
- public ForbidEncodedSlashesFilterFactory() {
- super();
- }
-
- @Override
- protected boolean shouldFilter(String uri) {
- return StringUtils.containsIgnoreCase(uri, ENCODED_SLASH);
- }
-
- @Override
- RuntimeException getException(String uri) {
- return new ForbidSlashException(uri);
- }
-}
diff --git a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ForbidSlashException.java b/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ForbidSlashException.java
deleted file mode 100644
index 070c562968..0000000000
--- a/gateway-service/src/main/java/org/zowe/apiml/gateway/filters/ForbidSlashException.java
+++ /dev/null
@@ -1,17 +0,0 @@
-/*
- * This program and the accompanying materials are made available under the terms of the
- * Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- * Copyright Contributors to the Zowe Project.
- */
-
-package org.zowe.apiml.gateway.filters;
-
-public class ForbidSlashException extends RuntimeException {
- public ForbidSlashException(String message) {
- super(message);
- }
-}
diff --git a/gateway-service/src/main/resources/application.yml b/gateway-service/src/main/resources/application.yml
index f14de44560..9b6b95d125 100644
--- a/gateway-service/src/main/resources/application.yml
+++ b/gateway-service/src/main/resources/application.yml
@@ -100,7 +100,6 @@ apiml:
apimlId: apiml1
corsEnabled: true
corsAllowedEndpoints: /gateway/**
- allowEncodedSlashes: true
discoveryServiceUrls: https://localhost:10011/eureka/
forwardClientCertEnabled: true
hostname: localhost
diff --git a/gateway-service/src/main/resources/gateway-log-messages.yml b/gateway-service/src/main/resources/gateway-log-messages.yml
index aabdaeb3ce..829b48d173 100644
--- a/gateway-service/src/main/resources/gateway-log-messages.yml
+++ b/gateway-service/src/main/resources/gateway-log-messages.yml
@@ -38,13 +38,6 @@ messages:
reason: "The request that was issued to the Gateway contains an encoded character in the URL path. The service that the request was addressing does not allow this pattern."
action: "Contact the system administrator and request enablement of encoded characters in the service."
- - key: org.zowe.apiml.gateway.requestContainEncodedSlash
- number: ZWEAG702
- type: ERROR
- text: "Gateway does not allow encoded slashes in request: '%s'."
- reason: "The request that was issued to the Gateway contains an encoded slash in the URL path. Gateway configuration does not allow this encoding in the URL."
- action: "Contact the system administrator and request enablement of encoded slashes in the Gateway."
-
- key: org.zowe.apiml.gateway.verifier.nonConformant
number: ZWEAG719
type: INFO
diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/StrictUrlValidationTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/StrictUrlValidationTest.java
new file mode 100644
index 0000000000..8e2e21c556
--- /dev/null
+++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/acceptance/StrictUrlValidationTest.java
@@ -0,0 +1,165 @@
+/*
+ * This program and the accompanying materials are made available under the terms of the
+ * Eclipse Public License v2.0 which accompanies this distribution, and is available at
+ * https://www.eclipse.org/legal/epl-v20.html
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ *
+ * Copyright Contributors to the Zowe Project.
+ */
+
+package org.zowe.apiml.gateway.acceptance;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Named;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.NestedTestConfiguration;
+import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.zowe.apiml.gateway.MockService;
+import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices;
+import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+import static io.restassured.RestAssured.given;
+import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.http.HttpStatus.SC_OK;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+/**
+ * Acceptance tests validating the {@code apiml.security.enableStrictUrlValidation} option in the Gateway. The two
+ * nested classes exercise the two states of the option, differentiated by {@link TestPropertySource}:
+ *
+ * - {@link WhenStrictValidationEnabled} - the default, strict URL validation rejects requests containing
+ * any of the encoded characters controlled by the option, whether the target is a routed service or a
+ * Gateway-internal endpoint.
+ * - {@link WhenStrictValidationDisabled} - relaxed URL validation forwards such requests to the target
+ * service for routed traffic, but Gateway-internal endpoints remain strictly validated.
+ *
+ * The parameterized cases cover every character relaxed by the option: encoded slash, encoded double slash,
+ * backslash, encoded percent, encoded period, and semicolon.
+ */
+@NestedTestConfiguration(EnclosingConfiguration.OVERRIDE)
+class StrictUrlValidationTest {
+
+ private static final String HEADER_X_FORWARD_TO = "X-Forward-To";
+ private static final String SERVICE_ID = "serviceid1";
+
+ /**
+ * Path suffixes, each containing one of the special characters controlled by
+ * {@code apiml.security.enableStrictUrlValidation}.
+ */
+ private static final List> SPECIAL_CHARACTERS = List.of(
+ Named.of("encoded slash", "encoded%2Fslash"),
+ Named.of("encoded double slash", "encoded%2F%2Fslash"),
+ Named.of("backslash", "encoded%5Cbackslash"),
+ Named.of("encoded percent", "encoded%25percent"),
+ Named.of("encoded period", "encoded%2Eperiod"),
+ Named.of("semicolon", "path;matrix")
+ );
+
+ /**
+ * Gateway-internal base paths, which are strictly validated even when the option is disabled. Matches
+ * {@code BASE_PATH_MICROSERVICES} in {@code WebSecurity}.
+ */
+ private static final List INTERNAL_ENDPOINTS = List.of(
+ "/gateway", "/application", "/images", "/v3/api-docs"
+ );
+
+ static Stream specialCharacters() {
+ return SPECIAL_CHARACTERS.stream().map(Arguments::arguments);
+ }
+
+ static Stream internalEndpointsWithSpecialCharacters() {
+ return INTERNAL_ENDPOINTS.stream().flatMap(endpoint ->
+ SPECIAL_CHARACTERS.stream().map(character -> arguments(endpoint, character)));
+ }
+
+ @Nested
+ @MicroservicesAcceptanceTest
+ @ActiveProfiles("test")
+ @TestPropertySource(properties = {
+ "apiml.security.enableStrictUrlValidation=true"
+ })
+ class WhenStrictValidationEnabled extends AcceptanceTestWithMockServices {
+
+ @BeforeAll
+ void setUp() {
+ mockService(SERVICE_ID).scope(MockService.Scope.CLASS)
+ .addEndpoint("/test")
+ .and().start();
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.zowe.apiml.gateway.acceptance.StrictUrlValidationTest#specialCharacters")
+ void whenRoutedRequestContainsSpecialCharacter_thenRejectedWithBadRequest(String pathSuffix) {
+ given()
+ .urlEncodingEnabled(false)
+ .header(HEADER_X_FORWARD_TO, SERVICE_ID)
+ .when()
+ .get(basePath + "/test/" + pathSuffix)
+ .then()
+ .statusCode(is(SC_BAD_REQUEST));
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.zowe.apiml.gateway.acceptance.StrictUrlValidationTest#internalEndpointsWithSpecialCharacters")
+ void whenInternalEndpointRequestContainsSpecialCharacter_thenRejectedWithBadRequest(String internalPath, String pathSuffix) {
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get(basePath + internalPath + "/" + pathSuffix)
+ .then()
+ .statusCode(is(SC_BAD_REQUEST));
+ }
+
+ }
+
+ @Nested
+ @MicroservicesAcceptanceTest
+ @ActiveProfiles("test")
+ @TestPropertySource(properties = {
+ "apiml.security.enableStrictUrlValidation=false"
+ })
+ class WhenStrictValidationDisabled extends AcceptanceTestWithMockServices {
+
+ @BeforeAll
+ void setUp() {
+ mockService(SERVICE_ID).scope(MockService.Scope.CLASS)
+ .addEndpoint("/test")
+ .and().start();
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.zowe.apiml.gateway.acceptance.StrictUrlValidationTest#specialCharacters")
+ void whenRoutedRequestContainsSpecialCharacter_thenForwardedToService(String pathSuffix) {
+ given()
+ .urlEncodingEnabled(false)
+ .header(HEADER_X_FORWARD_TO, SERVICE_ID)
+ .when()
+ .get(basePath + "/test/" + pathSuffix)
+ .then()
+ .statusCode(is(SC_OK));
+ }
+
+ @ParameterizedTest
+ @MethodSource("org.zowe.apiml.gateway.acceptance.StrictUrlValidationTest#internalEndpointsWithSpecialCharacters")
+ void whenInternalEndpointRequestContainsSpecialCharacter_thenStillRejectedWithBadRequest(String internalPath, String pathSuffix) {
+ given()
+ .urlEncodingEnabled(false)
+ .when()
+ .get(basePath + internalPath + "/" + pathSuffix)
+ .then()
+ .statusCode(is(SC_BAD_REQUEST));
+ }
+
+ }
+
+}
diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandlerTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandlerTest.java
index 3fe0f961d8..5ee3b98fed 100644
--- a/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandlerTest.java
+++ b/gateway-service/src/test/java/org/zowe/apiml/gateway/controllers/GatewayExceptionHandlerTest.java
@@ -35,7 +35,6 @@
import org.zowe.apiml.gateway.acceptance.common.MicroservicesAcceptanceTest;
import org.zowe.apiml.gateway.acceptance.common.AcceptanceTestWithMockServices;
import org.zowe.apiml.gateway.filters.ForbidCharacterException;
-import org.zowe.apiml.gateway.filters.ForbidSlashException;
import reactor.core.publisher.Mono;
import javax.net.ssl.SSLException;
@@ -90,7 +89,6 @@ void givenErrorResponse_whenCallGateway_thenDecorateIt(int code, String messageK
Stream getExceptions() {
return Stream.of(
- Arguments.of(new ForbidSlashException(""), 400, "org.zowe.apiml.gateway.requestContainEncodedSlash"),
Arguments.of(new ForbidCharacterException(""), 400, "org.zowe.apiml.gateway.requestContainEncodedCharacter"),
Arguments.of(new ResponseStatusException(HttpStatusCode.valueOf(504)), 504, "org.zowe.apiml.gateway.responseStatusError")
);
diff --git a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/ForbidEncodedSlashesFilterFactoryTest.java b/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/ForbidEncodedSlashesFilterFactoryTest.java
deleted file mode 100644
index 26e3d699e9..0000000000
--- a/gateway-service/src/test/java/org/zowe/apiml/gateway/filters/ForbidEncodedSlashesFilterFactoryTest.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * This program and the accompanying materials are made available under the terms of the
- * Eclipse Public License v2.0 which accompanies this distribution, and is available at
- * https://www.eclipse.org/legal/epl-v20.html
- *
- * SPDX-License-Identifier: EPL-2.0
- *
- * Copyright Contributors to the Zowe Project.
- */
-
-package org.zowe.apiml.gateway.filters;
-
-import org.junit.jupiter.api.Nested;
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.http.HttpMethod;
-import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
-import org.springframework.mock.web.server.MockServerWebExchange;
-import org.springframework.test.context.TestPropertySource;
-import reactor.core.publisher.Mono;
-
-import java.net.URI;
-import java.net.URISyntaxException;
-
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-@SpringBootTest
-class ForbidEncodedSlashesFilterFactoryTest {
-
- private static final String ENCODED_REQUEST_URI = "/api/v1/encoded%2fslash";
- private static final String NORMAL_REQUEST_URI = "/api/v1/normal";
-
- @Nested
- @TestPropertySource(properties = "apiml.service.allowEncodedSlashes=false")
- class Responses {
-
- @Autowired
- ForbidEncodedSlashesFilterFactory filter;
-
- @Test
- void givenNormalRequestUri_whenFilterApply_thenSuccess() {
- MockServerHttpRequest request = MockServerHttpRequest
- .get(NORMAL_REQUEST_URI)
- .build();
- MockServerWebExchange exchange = MockServerWebExchange.from(request);
-
- var response = filter.apply("").filter(exchange, e -> {
- exchange.getResponse().setRawStatusCode(200);
- return Mono.empty();
- });
- response.block();
- assertTrue(exchange.getResponse().getStatusCode().is2xxSuccessful());
- }
-
- @Test
- void givenRequestUriWithEncodedSlashes_whenFilterApply_thenReturnBadRequest() throws URISyntaxException {
- MockServerHttpRequest request = MockServerHttpRequest
- .method(HttpMethod.GET, new URI(ENCODED_REQUEST_URI))
- .build();
- MockServerWebExchange exchange = MockServerWebExchange.from(request);
-
- assertThrows(ForbidSlashException.class, () -> filter.apply("").filter(exchange, e -> Mono.empty()).block());
- }
-
- }
-
-}
diff --git a/gateway-service/src/test/resources/application.yml b/gateway-service/src/test/resources/application.yml
index 2269e800f0..8c78496ced 100644
--- a/gateway-service/src/test/resources/application.yml
+++ b/gateway-service/src/test/resources/application.yml
@@ -11,7 +11,6 @@ apiml:
hostname: localhost
scheme: https
corsEnabled: true
- allowEncodedSlashes: true
ignoredHeadersWhenCorsEnabled: Access-Control-Request-Method,Access-Control-Request-Headers,Access-Control-Allow-Origin,Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Credentials,Origin
gateway:
serviceRegistryEnabled: false
diff --git a/integration-tests/src/test/resources/environment-configuration-attls.yml b/integration-tests/src/test/resources/environment-configuration-attls.yml
index ec7d5df7ef..a7ee4b6fe9 100644
--- a/integration-tests/src/test/resources/environment-configuration-attls.yml
+++ b/integration-tests/src/test/resources/environment-configuration-attls.yml
@@ -70,7 +70,7 @@ instanceEnv:
CATALOG_PORT: 10014
DISCOVERY_PORT: 10011
GATEWAY_PORT: 10010
- APIML_ALLOW_ENCODED_SLASHES: true
+ ZWE_configs_apiml_security_enableStrictUrlValidation: false
APIML_PREFER_IP_ADDRESS: false
APIML_CONNECTION_TIMEOUT: 10000
APIML_SECURITY_X509_ENABLED: true
diff --git a/integration-tests/src/test/resources/environment-configuration-modulith.yml b/integration-tests/src/test/resources/environment-configuration-modulith.yml
index b38f9a731d..2672b7f2fb 100644
--- a/integration-tests/src/test/resources/environment-configuration-modulith.yml
+++ b/integration-tests/src/test/resources/environment-configuration-modulith.yml
@@ -69,7 +69,7 @@ instanceEnv:
CMMN_LB: build/libs/api-layer-lite-lib-all/BOOT-INF/lib/
ZWE_haInstance_hostname: localhost
ZOWE_PREFIX: ZWE
- ZWE_configs_apiml_service_allowEncodedSlashes: true
+ ZWE_configs_apiml_security_enableStrictUrlValidation: false
ZWE_configs_apiml_connection_timeout: 10000
ZWE_configs_apiml_security_x509_enabled: true
ZWE_configs_apiml_security_auth_provider: zosmf
diff --git a/integration-tests/src/test/resources/environment-configuration.yml b/integration-tests/src/test/resources/environment-configuration.yml
index a5cff6080f..832f1d837d 100644
--- a/integration-tests/src/test/resources/environment-configuration.yml
+++ b/integration-tests/src/test/resources/environment-configuration.yml
@@ -74,7 +74,7 @@ instanceEnv:
CMMN_LB: build/libs/api-layer-lite-lib-all/BOOT-INF/lib/
ZWE_haInstance_hostname: localhost
ZOWE_PREFIX: ZWE
- ZWE_configs_apiml_service_allowEncodedSlashes: true
+ ZWE_configs_apiml_security_enableStrictUrlValidation: false
ZWE_configs_apiml_connection_timeout: 10000
ZWE_configs_apiml_security_x509_enabled: true
ZWE_configs_apiml_security_auth_provider: zosmf