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}: + * + * 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