|  | @@ -0,0 +1,790 @@
 | 
	
		
			
				|  |  | +/*
 | 
	
		
			
				|  |  | + * Copyright 2002-2024 the original author or authors.
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * Licensed under the Apache License, Version 2.0 (the "License");
 | 
	
		
			
				|  |  | + * you may not use this file except in compliance with the License.
 | 
	
		
			
				|  |  | + * You may obtain a copy of the License at
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + *      https://www.apache.org/licenses/LICENSE-2.0
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * Unless required by applicable law or agreed to in writing, software
 | 
	
		
			
				|  |  | + * distributed under the License is distributed on an "AS IS" BASIS,
 | 
	
		
			
				|  |  | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
	
		
			
				|  |  | + * See the License for the specific language governing permissions and
 | 
	
		
			
				|  |  | + * limitations under the License.
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +package org.springframework.security.web.server.firewall;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import java.util.Arrays;
 | 
	
		
			
				|  |  | +import java.util.Collection;
 | 
	
		
			
				|  |  | +import java.util.Collections;
 | 
	
		
			
				|  |  | +import java.util.HashSet;
 | 
	
		
			
				|  |  | +import java.util.List;
 | 
	
		
			
				|  |  | +import java.util.Map;
 | 
	
		
			
				|  |  | +import java.util.Set;
 | 
	
		
			
				|  |  | +import java.util.function.Predicate;
 | 
	
		
			
				|  |  | +import java.util.regex.Pattern;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import reactor.core.publisher.Mono;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +import org.springframework.http.HttpHeaders;
 | 
	
		
			
				|  |  | +import org.springframework.http.HttpMethod;
 | 
	
		
			
				|  |  | +import org.springframework.http.server.reactive.ServerHttpRequest;
 | 
	
		
			
				|  |  | +import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
 | 
	
		
			
				|  |  | +import org.springframework.http.server.reactive.ServerHttpResponse;
 | 
	
		
			
				|  |  | +import org.springframework.util.Assert;
 | 
	
		
			
				|  |  | +import org.springframework.util.MultiValueMap;
 | 
	
		
			
				|  |  | +import org.springframework.web.server.ServerWebExchange;
 | 
	
		
			
				|  |  | +import org.springframework.web.server.ServerWebExchangeDecorator;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +/**
 | 
	
		
			
				|  |  | + * <p>
 | 
	
		
			
				|  |  | + * A strict implementation of {@link ServerWebExchangeFirewall} that rejects any
 | 
	
		
			
				|  |  | + * suspicious requests with a {@link ServerExchangeRejectedException}.
 | 
	
		
			
				|  |  | + * </p>
 | 
	
		
			
				|  |  | + * <p>
 | 
	
		
			
				|  |  | + * The following rules are applied to the firewall:
 | 
	
		
			
				|  |  | + * </p>
 | 
	
		
			
				|  |  | + * <ul>
 | 
	
		
			
				|  |  | + * <li>Rejects HTTP methods that are not allowed. This specified to block
 | 
	
		
			
				|  |  | + * <a href="https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)">HTTP Verb
 | 
	
		
			
				|  |  | + * tampering and XST attacks</a>. See {@link #setAllowedHttpMethods(Collection)}</li>
 | 
	
		
			
				|  |  | + * <li>Rejects URLs that are not normalized to avoid bypassing security constraints. There
 | 
	
		
			
				|  |  | + * is no way to disable this as it is considered extremely risky to disable this
 | 
	
		
			
				|  |  | + * constraint. A few options to allow this behavior is to normalize the request prior to
 | 
	
		
			
				|  |  | + * the firewall or using
 | 
	
		
			
				|  |  | + * {@link org.springframework.security.web.firewall.DefaultHttpFirewall} instead. Please
 | 
	
		
			
				|  |  | + * keep in mind that normalizing the request is fragile and why requests are rejected
 | 
	
		
			
				|  |  | + * rather than normalized.</li>
 | 
	
		
			
				|  |  | + * <li>Rejects URLs that contain characters that are not printable ASCII characters. There
 | 
	
		
			
				|  |  | + * is no way to disable this as it is considered extremely risky to disable this
 | 
	
		
			
				|  |  | + * constraint.</li>
 | 
	
		
			
				|  |  | + * <li>Rejects URLs that contain semicolons. See {@link #setAllowSemicolon(boolean)}</li>
 | 
	
		
			
				|  |  | + * <li>Rejects URLs that contain a URL encoded slash. See
 | 
	
		
			
				|  |  | + * {@link #setAllowUrlEncodedSlash(boolean)}</li>
 | 
	
		
			
				|  |  | + * <li>Rejects URLs that contain a backslash. See {@link #setAllowBackSlash(boolean)}</li>
 | 
	
		
			
				|  |  | + * <li>Rejects URLs that contain a null character. See {@link #setAllowNull(boolean)}</li>
 | 
	
		
			
				|  |  | + * <li>Rejects URLs that contain a URL encoded percent. See
 | 
	
		
			
				|  |  | + * {@link #setAllowUrlEncodedPercent(boolean)}</li>
 | 
	
		
			
				|  |  | + * <li>Rejects hosts that are not allowed. See {@link #setAllowedHostnames(Predicate)}
 | 
	
		
			
				|  |  | + * </li>
 | 
	
		
			
				|  |  | + * <li>Reject headers names that are not allowed. See
 | 
	
		
			
				|  |  | + * {@link #setAllowedHeaderNames(Predicate)}</li>
 | 
	
		
			
				|  |  | + * <li>Reject headers values that are not allowed. See
 | 
	
		
			
				|  |  | + * {@link #setAllowedHeaderValues(Predicate)}</li>
 | 
	
		
			
				|  |  | + * <li>Reject parameter names that are not allowed. See
 | 
	
		
			
				|  |  | + * {@link #setAllowedParameterNames(Predicate)}</li>
 | 
	
		
			
				|  |  | + * <li>Reject parameter values that are not allowed. See
 | 
	
		
			
				|  |  | + * {@link #setAllowedParameterValues(Predicate)}</li>
 | 
	
		
			
				|  |  | + * </ul>
 | 
	
		
			
				|  |  | + *
 | 
	
		
			
				|  |  | + * @author Rob Winch
 | 
	
		
			
				|  |  | + * @since 6.4
 | 
	
		
			
				|  |  | + */
 | 
	
		
			
				|  |  | +public class StrictServerWebExchangeFirewall implements ServerWebExchangeFirewall {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * Used to specify to {@link #setAllowedHttpMethods(Collection)} that any HTTP method
 | 
	
		
			
				|  |  | +	 * should be allowed.
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	private static final Set<HttpMethod> ALLOW_ANY_HTTP_METHOD = Collections.emptySet();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final String ENCODED_PERCENT = "%25";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final String PERCENT = "%";
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_ENCODED_PERIOD = Collections
 | 
	
		
			
				|  |  | +		.unmodifiableList(Arrays.asList("%2e", "%2E"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_SEMICOLON = Collections
 | 
	
		
			
				|  |  | +		.unmodifiableList(Arrays.asList(";", "%3b", "%3B"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_FORWARDSLASH = Collections
 | 
	
		
			
				|  |  | +		.unmodifiableList(Arrays.asList("%2f", "%2F"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_DOUBLE_FORWARDSLASH = Collections
 | 
	
		
			
				|  |  | +		.unmodifiableList(Arrays.asList("//", "%2f%2f", "%2f%2F", "%2F%2f", "%2F%2F"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_BACKSLASH = Collections
 | 
	
		
			
				|  |  | +		.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_NULL = Collections.unmodifiableList(Arrays.asList("\0", "%00"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_LF = Collections.unmodifiableList(Arrays.asList("\n", "%0a", "%0A"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_CR = Collections.unmodifiableList(Arrays.asList("\r", "%0d", "%0D"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_LINE_SEPARATOR = Collections.unmodifiableList(Arrays.asList("\u2028"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final List<String> FORBIDDEN_PARAGRAPH_SEPARATOR = Collections
 | 
	
		
			
				|  |  | +		.unmodifiableList(Arrays.asList("\u2029"));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private Set<String> encodedUrlBlocklist = new HashSet<>();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private Set<String> decodedUrlBlocklist = new HashSet<>();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private Set<HttpMethod> allowedHttpMethods = createDefaultAllowedHttpMethods();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private Predicate<String> allowedHostnames = (hostname) -> true;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final Pattern ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN = Pattern
 | 
	
		
			
				|  |  | +		.compile("[\\p{IsAssigned}&&[^\\p{IsControl}]]*");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final Predicate<String> ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE = (
 | 
	
		
			
				|  |  | +			s) -> ASSIGNED_AND_NOT_ISO_CONTROL_PATTERN.matcher(s).matches();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final Pattern HEADER_VALUE_PATTERN = Pattern.compile("[\\p{IsAssigned}&&[[^\\p{IsControl}]||\\t]]*");
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static final Predicate<String> HEADER_VALUE_PREDICATE = (s) -> s == null
 | 
	
		
			
				|  |  | +			|| HEADER_VALUE_PATTERN.matcher(s).matches();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private Predicate<String> allowedHeaderNames = ALLOWED_HEADER_NAMES;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	public static final Predicate<String> ALLOWED_HEADER_NAMES = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private Predicate<String> allowedHeaderValues = ALLOWED_HEADER_VALUES;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	public static final Predicate<String> ALLOWED_HEADER_VALUES = HEADER_VALUE_PREDICATE;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private Predicate<String> allowedParameterNames = ALLOWED_PARAMETER_NAMES;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	public static final Predicate<String> ALLOWED_PARAMETER_NAMES = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private Predicate<String> allowedParameterValues = ALLOWED_PARAMETER_VALUES;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	public static final Predicate<String> ALLOWED_PARAMETER_VALUES = (value) -> true;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	public StrictServerWebExchangeFirewall() {
 | 
	
		
			
				|  |  | +		urlBlocklistsAddAll(FORBIDDEN_SEMICOLON);
 | 
	
		
			
				|  |  | +		urlBlocklistsAddAll(FORBIDDEN_FORWARDSLASH);
 | 
	
		
			
				|  |  | +		urlBlocklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
 | 
	
		
			
				|  |  | +		urlBlocklistsAddAll(FORBIDDEN_BACKSLASH);
 | 
	
		
			
				|  |  | +		urlBlocklistsAddAll(FORBIDDEN_NULL);
 | 
	
		
			
				|  |  | +		urlBlocklistsAddAll(FORBIDDEN_LF);
 | 
	
		
			
				|  |  | +		urlBlocklistsAddAll(FORBIDDEN_CR);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		this.encodedUrlBlocklist.add(ENCODED_PERCENT);
 | 
	
		
			
				|  |  | +		this.encodedUrlBlocklist.addAll(FORBIDDEN_ENCODED_PERIOD);
 | 
	
		
			
				|  |  | +		this.decodedUrlBlocklist.add(PERCENT);
 | 
	
		
			
				|  |  | +		this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR);
 | 
	
		
			
				|  |  | +		this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	public Set<String> getEncodedUrlBlocklist() {
 | 
	
		
			
				|  |  | +		return this.encodedUrlBlocklist;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	public Set<String> getDecodedUrlBlocklist() {
 | 
	
		
			
				|  |  | +		return this.decodedUrlBlocklist;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	@Override
 | 
	
		
			
				|  |  | +	public Mono<ServerWebExchange> getFirewalledExchange(ServerWebExchange exchange) {
 | 
	
		
			
				|  |  | +		return Mono.fromCallable(() -> {
 | 
	
		
			
				|  |  | +			ServerHttpRequest request = exchange.getRequest();
 | 
	
		
			
				|  |  | +			rejectForbiddenHttpMethod(request);
 | 
	
		
			
				|  |  | +			rejectedBlocklistedUrls(request);
 | 
	
		
			
				|  |  | +			rejectedUntrustedHosts(request);
 | 
	
		
			
				|  |  | +			if (!isNormalized(request)) {
 | 
	
		
			
				|  |  | +				throw new ServerExchangeRejectedException(
 | 
	
		
			
				|  |  | +						"The request was rejected because the URL was not normalized");
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			exchange.getResponse().beforeCommit(() -> Mono.fromRunnable(() -> {
 | 
	
		
			
				|  |  | +				ServerHttpResponse response = exchange.getResponse();
 | 
	
		
			
				|  |  | +				HttpHeaders headers = response.getHeaders();
 | 
	
		
			
				|  |  | +				for (Map.Entry<String, List<String>> header : headers.entrySet()) {
 | 
	
		
			
				|  |  | +					String headerName = header.getKey();
 | 
	
		
			
				|  |  | +					List<String> headerValues = header.getValue();
 | 
	
		
			
				|  |  | +					for (String headerValue : headerValues) {
 | 
	
		
			
				|  |  | +						validateCrlf(headerName, headerValue);
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +			}));
 | 
	
		
			
				|  |  | +			return new StrictFirewallServerWebExchange(exchange);
 | 
	
		
			
				|  |  | +		});
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static void validateCrlf(String name, String value) {
 | 
	
		
			
				|  |  | +		Assert.isTrue(!hasCrlf(name) && !hasCrlf(value), () -> "Invalid characters (CR/LF) in header " + name);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static boolean hasCrlf(String value) {
 | 
	
		
			
				|  |  | +		return value != null && (value.indexOf('\n') != -1 || value.indexOf('\r') != -1);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * Sets if any HTTP method is allowed. If this set to true, then no validation on the
 | 
	
		
			
				|  |  | +	 * HTTP method will be performed. This can open the application up to
 | 
	
		
			
				|  |  | +	 * <a href="https://www.owasp.org/index.php/Test_HTTP_Methods_(OTG-CONFIG-006)"> HTTP
 | 
	
		
			
				|  |  | +	 * Verb tampering and XST attacks</a>
 | 
	
		
			
				|  |  | +	 * @param unsafeAllowAnyHttpMethod if true, disables HTTP method validation, else
 | 
	
		
			
				|  |  | +	 * resets back to the defaults. Default is false.
 | 
	
		
			
				|  |  | +	 * @since 5.1
 | 
	
		
			
				|  |  | +	 * @see #setAllowedHttpMethods(Collection)
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setUnsafeAllowAnyHttpMethod(boolean unsafeAllowAnyHttpMethod) {
 | 
	
		
			
				|  |  | +		this.allowedHttpMethods = unsafeAllowAnyHttpMethod ? ALLOW_ANY_HTTP_METHOD : createDefaultAllowedHttpMethods();
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines which HTTP methods should be allowed. The default is to allow "DELETE",
 | 
	
		
			
				|  |  | +	 * "GET", "HEAD", "OPTIONS", "PATCH", "POST", and "PUT".
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowedHttpMethods the case-sensitive collection of HTTP methods that are
 | 
	
		
			
				|  |  | +	 * allowed.
 | 
	
		
			
				|  |  | +	 * @since 5.1
 | 
	
		
			
				|  |  | +	 * @see #setUnsafeAllowAnyHttpMethod(boolean)
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowedHttpMethods(Collection<HttpMethod> allowedHttpMethods) {
 | 
	
		
			
				|  |  | +		Assert.notNull(allowedHttpMethods, "allowedHttpMethods cannot be null");
 | 
	
		
			
				|  |  | +		this.allowedHttpMethods = (allowedHttpMethods != ALLOW_ANY_HTTP_METHOD) ? new HashSet<>(allowedHttpMethods)
 | 
	
		
			
				|  |  | +				: ALLOW_ANY_HTTP_METHOD;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines if semicolon is allowed in the URL (i.e. matrix variables). The default
 | 
	
		
			
				|  |  | +	 * is to disable this behavior because it is a common way of attempting to perform
 | 
	
		
			
				|  |  | +	 * <a href="https://www.owasp.org/index.php/Reflected_File_Download">Reflected File
 | 
	
		
			
				|  |  | +	 * Download Attacks</a>. It is also the source of many exploits which bypass URL based
 | 
	
		
			
				|  |  | +	 * security.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * For example, the following CVEs are a subset of the issues related to ambiguities
 | 
	
		
			
				|  |  | +	 * in the Servlet Specification on how to treat semicolons that led to CVEs:
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * <ul>
 | 
	
		
			
				|  |  | +	 * <li><a href="https://pivotal.io/security/cve-2016-5007">cve-2016-5007</a></li>
 | 
	
		
			
				|  |  | +	 * <li><a href="https://pivotal.io/security/cve-2016-9879">cve-2016-9879</a></li>
 | 
	
		
			
				|  |  | +	 * <li><a href="https://pivotal.io/security/cve-2018-1199">cve-2018-1199</a></li>
 | 
	
		
			
				|  |  | +	 * </ul>
 | 
	
		
			
				|  |  | +	 *
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * If you are wanting to allow semicolons, please reconsider as it is a very common
 | 
	
		
			
				|  |  | +	 * source of security bypasses. A few common reasons users want semicolons and
 | 
	
		
			
				|  |  | +	 * alternatives are listed below:
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * <ul>
 | 
	
		
			
				|  |  | +	 * <li>Including the JSESSIONID in the path - You should not include session id (or
 | 
	
		
			
				|  |  | +	 * any sensitive information) in a URL as it can lead to leaking. Instead use Cookies.
 | 
	
		
			
				|  |  | +	 * </li>
 | 
	
		
			
				|  |  | +	 * <li>Matrix Variables - Users wanting to leverage Matrix Variables should consider
 | 
	
		
			
				|  |  | +	 * using HTTP parameters instead.</li>
 | 
	
		
			
				|  |  | +	 * </ul>
 | 
	
		
			
				|  |  | +	 * @param allowSemicolon should semicolons be allowed in the URL. Default is false
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowSemicolon(boolean allowSemicolon) {
 | 
	
		
			
				|  |  | +		if (allowSemicolon) {
 | 
	
		
			
				|  |  | +			urlBlocklistsRemoveAll(FORBIDDEN_SEMICOLON);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			urlBlocklistsAddAll(FORBIDDEN_SEMICOLON);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines if a slash "/" that is URL encoded "%2F" should be allowed in the path
 | 
	
		
			
				|  |  | +	 * or not. The default is to not allow this behavior because it is a common way to
 | 
	
		
			
				|  |  | +	 * bypass URL based security.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * For example, due to ambiguities in the servlet specification, the value is not
 | 
	
		
			
				|  |  | +	 * parsed consistently which results in different values in {@code HttpServletRequest}
 | 
	
		
			
				|  |  | +	 * path related values which allow bypassing certain security constraints.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowUrlEncodedSlash should a slash "/" that is URL encoded "%2F" be allowed
 | 
	
		
			
				|  |  | +	 * in the path or not. Default is false.
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowUrlEncodedSlash(boolean allowUrlEncodedSlash) {
 | 
	
		
			
				|  |  | +		if (allowUrlEncodedSlash) {
 | 
	
		
			
				|  |  | +			urlBlocklistsRemoveAll(FORBIDDEN_FORWARDSLASH);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			urlBlocklistsAddAll(FORBIDDEN_FORWARDSLASH);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines if double slash "//" that is URL encoded "%2F%2F" should be allowed in
 | 
	
		
			
				|  |  | +	 * the path or not. The default is to not allow.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowUrlEncodedDoubleSlash should a slash "//" that is URL encoded "%2F%2F"
 | 
	
		
			
				|  |  | +	 * be allowed in the path or not. Default is false.
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowUrlEncodedDoubleSlash(boolean allowUrlEncodedDoubleSlash) {
 | 
	
		
			
				|  |  | +		if (allowUrlEncodedDoubleSlash) {
 | 
	
		
			
				|  |  | +			urlBlocklistsRemoveAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			urlBlocklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines if a period "." that is URL encoded "%2E" should be allowed in the path
 | 
	
		
			
				|  |  | +	 * or not. The default is to not allow this behavior because it is a frequent source
 | 
	
		
			
				|  |  | +	 * of security exploits.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * For example, due to ambiguities in the servlet specification a URL encoded period
 | 
	
		
			
				|  |  | +	 * might lead to bypassing security constraints through a directory traversal attack.
 | 
	
		
			
				|  |  | +	 * This is because the path is not parsed consistently which results in different
 | 
	
		
			
				|  |  | +	 * values in {@code HttpServletRequest} path related values which allow bypassing
 | 
	
		
			
				|  |  | +	 * certain security constraints.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowUrlEncodedPeriod should a period "." that is URL encoded "%2E" be
 | 
	
		
			
				|  |  | +	 * allowed in the path or not. Default is false.
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowUrlEncodedPeriod(boolean allowUrlEncodedPeriod) {
 | 
	
		
			
				|  |  | +		if (allowUrlEncodedPeriod) {
 | 
	
		
			
				|  |  | +			this.encodedUrlBlocklist.removeAll(FORBIDDEN_ENCODED_PERIOD);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			this.encodedUrlBlocklist.addAll(FORBIDDEN_ENCODED_PERIOD);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines if a backslash "\" or a URL encoded backslash "%5C" should be allowed in
 | 
	
		
			
				|  |  | +	 * the path or not. The default is not to allow this behavior because it is a frequent
 | 
	
		
			
				|  |  | +	 * source of security exploits.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * For example, due to ambiguities in the servlet specification a URL encoded period
 | 
	
		
			
				|  |  | +	 * might lead to bypassing security constraints through a directory traversal attack.
 | 
	
		
			
				|  |  | +	 * This is because the path is not parsed consistently which results in different
 | 
	
		
			
				|  |  | +	 * values in {@code HttpServletRequest} path related values which allow bypassing
 | 
	
		
			
				|  |  | +	 * certain security constraints.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowBackSlash a backslash "\" or a URL encoded backslash "%5C" be allowed
 | 
	
		
			
				|  |  | +	 * in the path or not. Default is false
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowBackSlash(boolean allowBackSlash) {
 | 
	
		
			
				|  |  | +		if (allowBackSlash) {
 | 
	
		
			
				|  |  | +			urlBlocklistsRemoveAll(FORBIDDEN_BACKSLASH);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			urlBlocklistsAddAll(FORBIDDEN_BACKSLASH);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines if a null "\0" or a URL encoded nul "%00" should be allowed in the path
 | 
	
		
			
				|  |  | +	 * or not. The default is not to allow this behavior because it is a frequent source
 | 
	
		
			
				|  |  | +	 * of security exploits.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowNull a null "\0" or a URL encoded null "%00" be allowed in the path or
 | 
	
		
			
				|  |  | +	 * not. Default is false
 | 
	
		
			
				|  |  | +	 * @since 5.4
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowNull(boolean allowNull) {
 | 
	
		
			
				|  |  | +		if (allowNull) {
 | 
	
		
			
				|  |  | +			urlBlocklistsRemoveAll(FORBIDDEN_NULL);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			urlBlocklistsAddAll(FORBIDDEN_NULL);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines if a percent "%" that is URL encoded "%25" should be allowed in the path
 | 
	
		
			
				|  |  | +	 * or not. The default is not to allow this behavior because it is a frequent source
 | 
	
		
			
				|  |  | +	 * of security exploits.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * For example, this can lead to exploits that involve double URL encoding that lead
 | 
	
		
			
				|  |  | +	 * to bypassing security constraints.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowUrlEncodedPercent if a percent "%" that is URL encoded "%25" should be
 | 
	
		
			
				|  |  | +	 * allowed in the path or not. Default is false
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowUrlEncodedPercent(boolean allowUrlEncodedPercent) {
 | 
	
		
			
				|  |  | +		if (allowUrlEncodedPercent) {
 | 
	
		
			
				|  |  | +			this.encodedUrlBlocklist.remove(ENCODED_PERCENT);
 | 
	
		
			
				|  |  | +			this.decodedUrlBlocklist.remove(PERCENT);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			this.encodedUrlBlocklist.add(ENCODED_PERCENT);
 | 
	
		
			
				|  |  | +			this.decodedUrlBlocklist.add(PERCENT);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * Determines if a URL encoded Carriage Return is allowed in the path or not. The
 | 
	
		
			
				|  |  | +	 * default is not to allow this behavior because it is a frequent source of security
 | 
	
		
			
				|  |  | +	 * exploits.
 | 
	
		
			
				|  |  | +	 * @param allowUrlEncodedCarriageReturn if URL encoded Carriage Return is allowed in
 | 
	
		
			
				|  |  | +	 * the URL or not. Default is false.
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowUrlEncodedCarriageReturn(boolean allowUrlEncodedCarriageReturn) {
 | 
	
		
			
				|  |  | +		if (allowUrlEncodedCarriageReturn) {
 | 
	
		
			
				|  |  | +			urlBlocklistsRemoveAll(FORBIDDEN_CR);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			urlBlocklistsAddAll(FORBIDDEN_CR);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * Determines if a URL encoded Line Feed is allowed in the path or not. The default is
 | 
	
		
			
				|  |  | +	 * not to allow this behavior because it is a frequent source of security exploits.
 | 
	
		
			
				|  |  | +	 * @param allowUrlEncodedLineFeed if URL encoded Line Feed is allowed in the URL or
 | 
	
		
			
				|  |  | +	 * not. Default is false.
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowUrlEncodedLineFeed(boolean allowUrlEncodedLineFeed) {
 | 
	
		
			
				|  |  | +		if (allowUrlEncodedLineFeed) {
 | 
	
		
			
				|  |  | +			urlBlocklistsRemoveAll(FORBIDDEN_LF);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			urlBlocklistsAddAll(FORBIDDEN_LF);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * Determines if a URL encoded paragraph separator is allowed in the path or not. The
 | 
	
		
			
				|  |  | +	 * default is not to allow this behavior because it is a frequent source of security
 | 
	
		
			
				|  |  | +	 * exploits.
 | 
	
		
			
				|  |  | +	 * @param allowUrlEncodedParagraphSeparator if URL encoded paragraph separator is
 | 
	
		
			
				|  |  | +	 * allowed in the URL or not. Default is false.
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowUrlEncodedParagraphSeparator(boolean allowUrlEncodedParagraphSeparator) {
 | 
	
		
			
				|  |  | +		if (allowUrlEncodedParagraphSeparator) {
 | 
	
		
			
				|  |  | +			this.decodedUrlBlocklist.removeAll(FORBIDDEN_PARAGRAPH_SEPARATOR);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			this.decodedUrlBlocklist.addAll(FORBIDDEN_PARAGRAPH_SEPARATOR);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * Determines if a URL encoded line separator is allowed in the path or not. The
 | 
	
		
			
				|  |  | +	 * default is not to allow this behavior because it is a frequent source of security
 | 
	
		
			
				|  |  | +	 * exploits.
 | 
	
		
			
				|  |  | +	 * @param allowUrlEncodedLineSeparator if URL encoded line separator is allowed in the
 | 
	
		
			
				|  |  | +	 * URL or not. Default is false.
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowUrlEncodedLineSeparator(boolean allowUrlEncodedLineSeparator) {
 | 
	
		
			
				|  |  | +		if (allowUrlEncodedLineSeparator) {
 | 
	
		
			
				|  |  | +			this.decodedUrlBlocklist.removeAll(FORBIDDEN_LINE_SEPARATOR);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		else {
 | 
	
		
			
				|  |  | +			this.decodedUrlBlocklist.addAll(FORBIDDEN_LINE_SEPARATOR);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines which header names should be allowed. The default is to reject header
 | 
	
		
			
				|  |  | +	 * names that contain ISO control characters and characters that are not defined.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowedHeaderNames the predicate for testing header names
 | 
	
		
			
				|  |  | +	 * @since 5.4
 | 
	
		
			
				|  |  | +	 * @see Character#isISOControl(int)
 | 
	
		
			
				|  |  | +	 * @see Character#isDefined(int)
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowedHeaderNames(Predicate<String> allowedHeaderNames) {
 | 
	
		
			
				|  |  | +		Assert.notNull(allowedHeaderNames, "allowedHeaderNames cannot be null");
 | 
	
		
			
				|  |  | +		this.allowedHeaderNames = allowedHeaderNames;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines which header values should be allowed. The default is to reject header
 | 
	
		
			
				|  |  | +	 * values that contain ISO control characters and characters that are not defined.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowedHeaderValues the predicate for testing hostnames
 | 
	
		
			
				|  |  | +	 * @since 5.4
 | 
	
		
			
				|  |  | +	 * @see Character#isISOControl(int)
 | 
	
		
			
				|  |  | +	 * @see Character#isDefined(int)
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowedHeaderValues(Predicate<String> allowedHeaderValues) {
 | 
	
		
			
				|  |  | +		Assert.notNull(allowedHeaderValues, "allowedHeaderValues cannot be null");
 | 
	
		
			
				|  |  | +		this.allowedHeaderValues = allowedHeaderValues;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * Determines which parameter names should be allowed. The default is to reject header
 | 
	
		
			
				|  |  | +	 * names that contain ISO control characters and characters that are not defined.
 | 
	
		
			
				|  |  | +	 * @param allowedParameterNames the predicate for testing parameter names
 | 
	
		
			
				|  |  | +	 * @since 5.4
 | 
	
		
			
				|  |  | +	 * @see Character#isISOControl(int)
 | 
	
		
			
				|  |  | +	 * @see Character#isDefined(int)
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowedParameterNames(Predicate<String> allowedParameterNames) {
 | 
	
		
			
				|  |  | +		Assert.notNull(allowedParameterNames, "allowedParameterNames cannot be null");
 | 
	
		
			
				|  |  | +		this.allowedParameterNames = allowedParameterNames;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines which parameter values should be allowed. The default is to allow any
 | 
	
		
			
				|  |  | +	 * parameter value.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowedParameterValues the predicate for testing parameter values
 | 
	
		
			
				|  |  | +	 * @since 5.4
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowedParameterValues(Predicate<String> allowedParameterValues) {
 | 
	
		
			
				|  |  | +		Assert.notNull(allowedParameterValues, "allowedParameterValues cannot be null");
 | 
	
		
			
				|  |  | +		this.allowedParameterValues = allowedParameterValues;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * <p>
 | 
	
		
			
				|  |  | +	 * Determines which hostnames should be allowed. The default is to allow any hostname.
 | 
	
		
			
				|  |  | +	 * </p>
 | 
	
		
			
				|  |  | +	 * @param allowedHostnames the predicate for testing hostnames
 | 
	
		
			
				|  |  | +	 * @since 5.2
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	public void setAllowedHostnames(Predicate<String> allowedHostnames) {
 | 
	
		
			
				|  |  | +		Assert.notNull(allowedHostnames, "allowedHostnames cannot be null");
 | 
	
		
			
				|  |  | +		this.allowedHostnames = allowedHostnames;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void urlBlocklistsAddAll(Collection<String> values) {
 | 
	
		
			
				|  |  | +		this.encodedUrlBlocklist.addAll(values);
 | 
	
		
			
				|  |  | +		this.decodedUrlBlocklist.addAll(values);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void urlBlocklistsRemoveAll(Collection<String> values) {
 | 
	
		
			
				|  |  | +		this.encodedUrlBlocklist.removeAll(values);
 | 
	
		
			
				|  |  | +		this.decodedUrlBlocklist.removeAll(values);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void rejectNonPrintableAsciiCharactersInFieldName(String toCheck, String propertyName) {
 | 
	
		
			
				|  |  | +		if (!containsOnlyPrintableAsciiCharacters(toCheck)) {
 | 
	
		
			
				|  |  | +			throw new ServerExchangeRejectedException(String
 | 
	
		
			
				|  |  | +				.format("The %s was rejected because it can only contain printable ASCII characters.", propertyName));
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void rejectForbiddenHttpMethod(ServerHttpRequest request) {
 | 
	
		
			
				|  |  | +		if (this.allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
 | 
	
		
			
				|  |  | +			return;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		if (!this.allowedHttpMethods.contains(request.getMethod())) {
 | 
	
		
			
				|  |  | +			throw new ServerExchangeRejectedException(
 | 
	
		
			
				|  |  | +					"The request was rejected because the HTTP method \"" + request.getMethod()
 | 
	
		
			
				|  |  | +							+ "\" was not included within the list of allowed HTTP methods " + this.allowedHttpMethods);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void rejectedBlocklistedUrls(ServerHttpRequest request) {
 | 
	
		
			
				|  |  | +		for (String forbidden : this.encodedUrlBlocklist) {
 | 
	
		
			
				|  |  | +			if (encodedUrlContains(request, forbidden)) {
 | 
	
		
			
				|  |  | +				throw new ServerExchangeRejectedException(
 | 
	
		
			
				|  |  | +						"The request was rejected because the URL contained a potentially malicious String \""
 | 
	
		
			
				|  |  | +								+ forbidden + "\"");
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		for (String forbidden : this.decodedUrlBlocklist) {
 | 
	
		
			
				|  |  | +			if (decodedUrlContains(request, forbidden)) {
 | 
	
		
			
				|  |  | +				throw new ServerExchangeRejectedException(
 | 
	
		
			
				|  |  | +						"The request was rejected because the URL contained a potentially malicious String \""
 | 
	
		
			
				|  |  | +								+ forbidden + "\"");
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void rejectedUntrustedHosts(ServerHttpRequest request) {
 | 
	
		
			
				|  |  | +		String hostName = request.getURI().getHost();
 | 
	
		
			
				|  |  | +		if (hostName != null && !this.allowedHostnames.test(hostName)) {
 | 
	
		
			
				|  |  | +			throw new ServerExchangeRejectedException(
 | 
	
		
			
				|  |  | +					"The request was rejected because the domain " + hostName + " is untrusted.");
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static Set<HttpMethod> createDefaultAllowedHttpMethods() {
 | 
	
		
			
				|  |  | +		Set<HttpMethod> result = new HashSet<>();
 | 
	
		
			
				|  |  | +		result.add(HttpMethod.DELETE);
 | 
	
		
			
				|  |  | +		result.add(HttpMethod.GET);
 | 
	
		
			
				|  |  | +		result.add(HttpMethod.HEAD);
 | 
	
		
			
				|  |  | +		result.add(HttpMethod.OPTIONS);
 | 
	
		
			
				|  |  | +		result.add(HttpMethod.PATCH);
 | 
	
		
			
				|  |  | +		result.add(HttpMethod.POST);
 | 
	
		
			
				|  |  | +		result.add(HttpMethod.PUT);
 | 
	
		
			
				|  |  | +		return result;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private boolean isNormalized(ServerHttpRequest request) {
 | 
	
		
			
				|  |  | +		if (!isNormalized(request.getPath().value())) {
 | 
	
		
			
				|  |  | +			return false;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		if (!isNormalized(request.getURI().getRawPath())) {
 | 
	
		
			
				|  |  | +			return false;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		if (!isNormalized(request.getURI().getPath())) {
 | 
	
		
			
				|  |  | +			return false;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		return true;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void validateAllowedHeaderName(String headerNames) {
 | 
	
		
			
				|  |  | +		if (!StrictServerWebExchangeFirewall.this.allowedHeaderNames.test(headerNames)) {
 | 
	
		
			
				|  |  | +			throw new ServerExchangeRejectedException(
 | 
	
		
			
				|  |  | +					"The request was rejected because the header name \"" + headerNames + "\" is not allowed.");
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void validateAllowedHeaderValue(Object key, String value) {
 | 
	
		
			
				|  |  | +		if (!StrictServerWebExchangeFirewall.this.allowedHeaderValues.test(value)) {
 | 
	
		
			
				|  |  | +			throw new ServerExchangeRejectedException("The request was rejected because the header: \"" + key
 | 
	
		
			
				|  |  | +					+ " \" has a value \"" + value + "\" that is not allowed.");
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void validateAllowedParameterName(String name) {
 | 
	
		
			
				|  |  | +		if (!StrictServerWebExchangeFirewall.this.allowedParameterNames.test(name)) {
 | 
	
		
			
				|  |  | +			throw new ServerExchangeRejectedException(
 | 
	
		
			
				|  |  | +					"The request was rejected because the parameter name \"" + name + "\" is not allowed.");
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private void validateAllowedParameterValue(String name, String value) {
 | 
	
		
			
				|  |  | +		if (!StrictServerWebExchangeFirewall.this.allowedParameterValues.test(value)) {
 | 
	
		
			
				|  |  | +			throw new ServerExchangeRejectedException("The request was rejected because the parameter: \"" + name
 | 
	
		
			
				|  |  | +					+ " \" has a value \"" + value + "\" that is not allowed.");
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static boolean encodedUrlContains(ServerHttpRequest request, String value) {
 | 
	
		
			
				|  |  | +		if (valueContains(request.getPath().value(), value)) {
 | 
	
		
			
				|  |  | +			return true;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		return valueContains(request.getURI().getRawPath(), value);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static boolean decodedUrlContains(ServerHttpRequest request, String value) {
 | 
	
		
			
				|  |  | +		return valueContains(request.getURI().getPath(), value);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static boolean containsOnlyPrintableAsciiCharacters(String uri) {
 | 
	
		
			
				|  |  | +		if (uri == null) {
 | 
	
		
			
				|  |  | +			return true;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		int length = uri.length();
 | 
	
		
			
				|  |  | +		for (int i = 0; i < length; i++) {
 | 
	
		
			
				|  |  | +			char ch = uri.charAt(i);
 | 
	
		
			
				|  |  | +			if (ch < '\u0020' || ch > '\u007e') {
 | 
	
		
			
				|  |  | +				return false;
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		return true;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private static boolean valueContains(String value, String contains) {
 | 
	
		
			
				|  |  | +		return value != null && value.contains(contains);
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	/**
 | 
	
		
			
				|  |  | +	 * Checks whether a path is normalized (doesn't contain path traversal sequences like
 | 
	
		
			
				|  |  | +	 * "./", "/../" or "/.")
 | 
	
		
			
				|  |  | +	 * @param path the path to test
 | 
	
		
			
				|  |  | +	 * @return true if the path doesn't contain any path-traversal character sequences.
 | 
	
		
			
				|  |  | +	 */
 | 
	
		
			
				|  |  | +	private static boolean isNormalized(String path) {
 | 
	
		
			
				|  |  | +		if (path == null) {
 | 
	
		
			
				|  |  | +			return true;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		for (int i = path.length(); i > 0;) {
 | 
	
		
			
				|  |  | +			int slashIndex = path.lastIndexOf('/', i - 1);
 | 
	
		
			
				|  |  | +			int gap = i - slashIndex;
 | 
	
		
			
				|  |  | +			if (gap == 2 && path.charAt(slashIndex + 1) == '.') {
 | 
	
		
			
				|  |  | +				return false; // ".", "/./" or "/."
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +			if (gap == 3 && path.charAt(slashIndex + 1) == '.' && path.charAt(slashIndex + 2) == '.') {
 | 
	
		
			
				|  |  | +				return false;
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +			i = slashIndex;
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +		return true;
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	private final class StrictFirewallServerWebExchange extends ServerWebExchangeDecorator {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		private StrictFirewallServerWebExchange(ServerWebExchange delegate) {
 | 
	
		
			
				|  |  | +			super(delegate);
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		@Override
 | 
	
		
			
				|  |  | +		public ServerHttpRequest getRequest() {
 | 
	
		
			
				|  |  | +			return new StrictFirewallHttpRequest(super.getRequest());
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		private final class StrictFirewallHttpRequest extends ServerHttpRequestDecorator {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			private StrictFirewallHttpRequest(ServerHttpRequest delegate) {
 | 
	
		
			
				|  |  | +				super(delegate);
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			@Override
 | 
	
		
			
				|  |  | +			public HttpHeaders getHeaders() {
 | 
	
		
			
				|  |  | +				return new StrictFirewallHttpHeaders(super.getHeaders());
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			@Override
 | 
	
		
			
				|  |  | +			public MultiValueMap<String, String> getQueryParams() {
 | 
	
		
			
				|  |  | +				MultiValueMap<String, String> queryParams = super.getQueryParams();
 | 
	
		
			
				|  |  | +				for (Map.Entry<String, List<String>> paramEntry : queryParams.entrySet()) {
 | 
	
		
			
				|  |  | +					String paramName = paramEntry.getKey();
 | 
	
		
			
				|  |  | +					validateAllowedParameterName(paramName);
 | 
	
		
			
				|  |  | +					for (String paramValue : paramEntry.getValue()) {
 | 
	
		
			
				|  |  | +						validateAllowedParameterValue(paramName, paramValue);
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +				return queryParams;
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			private final class StrictFirewallHttpHeaders extends HttpHeaders {
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +				private StrictFirewallHttpHeaders(HttpHeaders delegate) {
 | 
	
		
			
				|  |  | +					super(delegate);
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +				@Override
 | 
	
		
			
				|  |  | +				public String getFirst(String headerName) {
 | 
	
		
			
				|  |  | +					validateAllowedHeaderName(headerName);
 | 
	
		
			
				|  |  | +					String headerValue = super.getFirst(headerName);
 | 
	
		
			
				|  |  | +					validateAllowedHeaderValue(headerName, headerValue);
 | 
	
		
			
				|  |  | +					return headerValue;
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +				@Override
 | 
	
		
			
				|  |  | +				public List<String> get(Object key) {
 | 
	
		
			
				|  |  | +					if (key instanceof String headerName) {
 | 
	
		
			
				|  |  | +						validateAllowedHeaderName(headerName);
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +					List<String> headerValues = super.get(key);
 | 
	
		
			
				|  |  | +					if (headerValues == null) {
 | 
	
		
			
				|  |  | +						return headerValues;
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +					for (String headerValue : headerValues) {
 | 
	
		
			
				|  |  | +						validateAllowedHeaderValue(key, headerValue);
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +					return headerValues;
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +				@Override
 | 
	
		
			
				|  |  | +				public Set<String> keySet() {
 | 
	
		
			
				|  |  | +					Set<String> headerNames = super.keySet();
 | 
	
		
			
				|  |  | +					for (String headerName : headerNames) {
 | 
	
		
			
				|  |  | +						validateAllowedHeaderName(headerName);
 | 
	
		
			
				|  |  | +					}
 | 
	
		
			
				|  |  | +					return headerNames;
 | 
	
		
			
				|  |  | +				}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +			}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +		}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +	}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +}
 |