浏览代码

StrictHttpFirewall allows CJKV characters

Closes gh-11264
Rob Winch 3 年之前
父节点
当前提交
cfc057b629

+ 523 - 180
web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2019 the original author or authors.
+ * Copyright 2012-2021 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.
@@ -19,14 +19,19 @@ package org.springframework.security.web.firewall;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Enumeration;
 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 javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.springframework.http.HttpMethod;
+import org.springframework.util.Assert;
 
 /**
  * <p>
@@ -37,93 +42,127 @@ import org.springframework.http.HttpMethod;
  * 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 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 URL encoded percent. See
- * {@link #setAllowUrlEncodedPercent(boolean)}
- * </li>
- * <li>
- * Rejects hosts that are not allowed. See
- * {@link #setAllowedHostnames(Predicate)}
+ * <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 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>
  *
- * @see DefaultHttpFirewall
  * @author Rob Winch
  * @author Eddú Meléndez
  * @since 4.2.4
+ * @see DefaultHttpFirewall
  */
 public class StrictHttpFirewall implements HttpFirewall {
+
 	/**
-	 * Used to specify to {@link #setAllowedHttpMethods(Collection)} that any HTTP method should be allowed.
+	 * Used to specify to {@link #setAllowedHttpMethods(Collection)} that any HTTP method
+	 * should be allowed.
 	 */
-	private static final Set<String> ALLOW_ANY_HTTP_METHOD = Collections.unmodifiableSet(Collections.emptySet());
+	private static final Set<String> 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_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_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_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_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_BACKSLASH = Collections.unmodifiableList(Arrays.asList("\\", "%5c", "%5C"));
+	private static final List<String> FORBIDDEN_NULL = Collections.unmodifiableList(Arrays.asList("\0", "%00"));
 
-	private Set<String> encodedUrlBlacklist = new HashSet<>();
+	private static final List<String> FORBIDDEN_LF = Collections.unmodifiableList(Arrays.asList("\n", "%0a", "%0A"));
 
-	private Set<String> decodedUrlBlacklist = new HashSet<>();
+	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<String> allowedHttpMethods = createDefaultAllowedHttpMethods();
 
-	private Predicate<String> allowedHostnames = hostname -> true;
+	private Predicate<String> allowedHostnames = (hostname) -> true;
 
-	public StrictHttpFirewall() {
-		urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
-		urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
-		urlBlacklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
-		urlBlacklistsAddAll(FORBIDDEN_BACKSLASH);
+	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();
 
-		this.encodedUrlBlacklist.add(ENCODED_PERCENT);
-		this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD);
-		this.decodedUrlBlacklist.add(PERCENT);
+	private Predicate<String> allowedHeaderNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
+
+	private Predicate<String> allowedHeaderValues = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
+
+	private Predicate<String> allowedParameterNames = ASSIGNED_AND_NOT_ISO_CONTROL_PREDICATE;
+
+	private Predicate<String> allowedParameterValues = (value) -> true;
+
+	public StrictHttpFirewall() {
+		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);
 	}
 
 	/**
-	 * 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.
-	 * @see #setAllowedHttpMethods(Collection)
+	 * 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();
@@ -131,40 +170,36 @@ public class StrictHttpFirewall implements HttpFirewall {
 
 	/**
 	 * <p>
-	 * Determines which HTTP methods should be allowed. The default is to allow "DELETE", "GET", "HEAD", "OPTIONS",
-	 * "PATCH", "POST", and "PUT".
+	 * 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.
-	 * @see #setUnsafeAllowAnyHttpMethod(boolean)
+	 * @param allowedHttpMethods the case-sensitive collection of HTTP methods that are
+	 * allowed.
 	 * @since 5.1
+	 * @see #setUnsafeAllowAnyHttpMethod(boolean)
 	 */
 	public void setAllowedHttpMethods(Collection<String> allowedHttpMethods) {
-		if (allowedHttpMethods == null) {
-			throw new IllegalArgumentException("allowedHttpMethods cannot be null");
-		}
-		if (allowedHttpMethods == ALLOW_ANY_HTTP_METHOD) {
-			this.allowedHttpMethods = ALLOW_ANY_HTTP_METHOD;
-		} else {
-			this.allowedHttpMethods = new HashSet<>(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.
+	 * <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>
+	 * 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>
+	 * <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>
@@ -177,17 +212,16 @@ public class StrictHttpFirewall implements HttpFirewall {
 	 * 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>
+	 * 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) {
-			urlBlacklistsRemoveAll(FORBIDDEN_SEMICOLON);
-		} else {
-			urlBlacklistsAddAll(FORBIDDEN_SEMICOLON);
+			urlBlocklistsRemoveAll(FORBIDDEN_SEMICOLON);
+		}
+		else {
+			urlBlocklistsAddAll(FORBIDDEN_SEMICOLON);
 		}
 	}
 
@@ -202,32 +236,32 @@ public class StrictHttpFirewall implements HttpFirewall {
 	 * 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) {
-			urlBlacklistsRemoveAll(FORBIDDEN_FORWARDSLASH);
-		} else {
-			urlBlacklistsAddAll(FORBIDDEN_FORWARDSLASH);
+			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.
+	 * 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.
+	 * @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) {
-			urlBlacklistsRemoveAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
-		} else {
-			urlBlacklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
+			urlBlocklistsRemoveAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
+		}
+		else {
+			urlBlocklistsAddAll(FORBIDDEN_DOUBLE_FORWARDSLASH);
 		}
 	}
 
@@ -240,19 +274,19 @@ public class StrictHttpFirewall implements HttpFirewall {
 	 * <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
+	 * 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.encodedUrlBlacklist.removeAll(FORBIDDEN_ENCODED_PERIOD);
-		} else {
-			this.encodedUrlBlacklist.addAll(FORBIDDEN_ENCODED_PERIOD);
+			this.encodedUrlBlocklist.removeAll(FORBIDDEN_ENCODED_PERIOD);
+		}
+		else {
+			this.encodedUrlBlocklist.addAll(FORBIDDEN_ENCODED_PERIOD);
 		}
 	}
 
@@ -265,19 +299,38 @@ public class StrictHttpFirewall implements HttpFirewall {
 	 * <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
+	 * 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) {
-			urlBlacklistsRemoveAll(FORBIDDEN_BACKSLASH);
-		} else {
-			urlBlacklistsAddAll(FORBIDDEN_BACKSLASH);
+			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.3.14
+	 */
+	public void setAllowNull(boolean allowNull) {
+		if (allowNull) {
+			urlBlocklistsRemoveAll(FORBIDDEN_NULL);
+		}
+		else {
+			urlBlocklistsAddAll(FORBIDDEN_NULL);
 		}
 	}
 
@@ -291,75 +344,177 @@ public class StrictHttpFirewall implements HttpFirewall {
 	 * 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.encodedUrlBlacklist.remove(ENCODED_PERCENT);
-			this.decodedUrlBlacklist.remove(PERCENT);
-		} else {
-			this.encodedUrlBlacklist.add(ENCODED_PERCENT);
-			this.decodedUrlBlacklist.add(PERCENT);
+			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.3.14
+	 * @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.3.14
+	 * @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.3.14
+	 * @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.3.14
+	 */
+	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) {
-		if (allowedHostnames == null) {
-			throw new IllegalArgumentException("allowedHostnames cannot be null");
-		}
+		Assert.notNull(allowedHostnames, "allowedHostnames cannot be null");
 		this.allowedHostnames = allowedHostnames;
 	}
 
-	private void urlBlacklistsAddAll(Collection<String> values) {
-		this.encodedUrlBlacklist.addAll(values);
-		this.decodedUrlBlacklist.addAll(values);
+	private void urlBlocklistsAddAll(Collection<String> values) {
+		this.encodedUrlBlocklist.addAll(values);
+		this.decodedUrlBlocklist.addAll(values);
 	}
 
-	private void urlBlacklistsRemoveAll(Collection<String> values) {
-		this.encodedUrlBlacklist.removeAll(values);
-		this.decodedUrlBlacklist.removeAll(values);
+	private void urlBlocklistsRemoveAll(Collection<String> values) {
+		this.encodedUrlBlocklist.removeAll(values);
+		this.decodedUrlBlocklist.removeAll(values);
 	}
 
 	@Override
 	public FirewalledRequest getFirewalledRequest(HttpServletRequest request) throws RequestRejectedException {
 		rejectForbiddenHttpMethod(request);
-		rejectedBlacklistedUrls(request);
+		rejectedBlocklistedUrls(request);
 		rejectedUntrustedHosts(request);
-
 		if (!isNormalized(request)) {
 			throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
 		}
-
-		String requestUri = request.getRequestURI();
-		if (!containsOnlyPrintableAsciiCharacters(requestUri)) {
-			throw new RequestRejectedException("The requestURI was rejected because it can only contain printable ASCII characters.");
-		}
 		rejectNonPrintableAsciiCharactersInFieldName(request.getRequestURI(), "requestURI");
-		rejectNonPrintableAsciiCharactersInFieldName(request.getServletPath(), "servletPath");
-		rejectNonPrintableAsciiCharactersInFieldName(request.getPathInfo(), "pathInfo");
-		rejectNonPrintableAsciiCharactersInFieldName(request.getContextPath(), "contextPath");
-
-		return new FirewalledRequest(request) {
-			@Override
-			public void reset() {
-			}
-		};
+		return new StrictFirewalledRequest(request);
 	}
 
 	private void rejectNonPrintableAsciiCharactersInFieldName(String toCheck, String propertyName) {
 		if (!containsOnlyPrintableAsciiCharacters(toCheck)) {
-			throw new RequestRejectedException(
-					String.format("The %s was rejected because it can only contain printable ASCII characters.", propertyName));
+			throw new RequestRejectedException(String.format(
+					"The %s was rejected because it can only contain printable ASCII characters.", propertyName));
 		}
 	}
 
@@ -368,22 +523,25 @@ public class StrictHttpFirewall implements HttpFirewall {
 			return;
 		}
 		if (!this.allowedHttpMethods.contains(request.getMethod())) {
-			throw new RequestRejectedException("The request was rejected because the HTTP method \"" +
-					request.getMethod() +
-					"\" was not included within the whitelist " +
-					this.allowedHttpMethods);
+			throw new RequestRejectedException(
+					"The request was rejected because the HTTP method \"" + request.getMethod()
+							+ "\" was not included within the list of allowed HTTP methods " + this.allowedHttpMethods);
 		}
 	}
 
-	private void rejectedBlacklistedUrls(HttpServletRequest request) {
-		for (String forbidden : this.encodedUrlBlacklist) {
+	private void rejectedBlocklistedUrls(HttpServletRequest request) {
+		for (String forbidden : this.encodedUrlBlocklist) {
 			if (encodedUrlContains(request, forbidden)) {
-				throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
+				throw new RequestRejectedException(
+						"The request was rejected because the URL contained a potentially malicious String \""
+								+ forbidden + "\"");
 			}
 		}
-		for (String forbidden : this.decodedUrlBlacklist) {
+		for (String forbidden : this.decodedUrlBlocklist) {
 			if (decodedUrlContains(request, forbidden)) {
-				throw new RequestRejectedException("The request was rejected because the URL contained a potentially malicious String \"" + forbidden + "\"");
+				throw new RequestRejectedException(
+						"The request was rejected because the URL contained a potentially malicious String \""
+								+ forbidden + "\"");
 			}
 		}
 	}
@@ -391,7 +549,8 @@ public class StrictHttpFirewall implements HttpFirewall {
 	private void rejectedUntrustedHosts(HttpServletRequest request) {
 		String serverName = request.getServerName();
 		if (serverName != null && !this.allowedHostnames.test(serverName)) {
-			throw new RequestRejectedException("The request was rejected because the domain " + serverName + " is untrusted.");
+			throw new RequestRejectedException(
+					"The request was rejected because the domain " + serverName + " is untrusted.");
 		}
 	}
 
@@ -451,12 +610,11 @@ public class StrictHttpFirewall implements HttpFirewall {
 		}
 		int length = uri.length();
 		for (int i = 0; i < length; i++) {
-			char c = uri.charAt(i);
-			if (c < '\u0020' || c > '\u007e') {
+			char ch = uri.charAt(i);
+			if (ch < '\u0020' || ch > '\u007e') {
 				return false;
 			}
 		}
-
 		return true;
 	}
 
@@ -465,51 +623,236 @@ public class StrictHttpFirewall implements HttpFirewall {
 	}
 
 	/**
-	 * 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.
+	 * 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 j = path.length(); j > 0;) {
-			int i = path.lastIndexOf('/', j - 1);
-			int gap = j - i;
-
-			if (gap == 2 && path.charAt(i + 1) == '.') {
-				// ".", "/./" or "/."
-				return false;
-			} else if (gap == 3 && path.charAt(i + 1) == '.' && path.charAt(i + 2) == '.') {
+		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;
 			}
-
-			j = i;
+			i = slashIndex;
 		}
-
 		return true;
 	}
 
 	/**
-	 * Provides the existing encoded url blacklist which can add/remove entries from
-	 *
-	 * @return the existing encoded url blacklist, never null
+	 * Provides the existing encoded url blocklist which can add/remove entries from
+	 * @return the existing encoded url blocklist, never null
+	 */
+	public Set<String> getEncodedUrlBlocklist() {
+		return this.encodedUrlBlocklist;
+	}
+
+	/**
+	 * Provides the existing decoded url blocklist which can add/remove entries from
+	 * @return the existing decoded url blocklist, never null
 	 */
+	public Set<String> getDecodedUrlBlocklist() {
+		return this.decodedUrlBlocklist;
+	}
+
+	/**
+	 * Provides the existing encoded url blocklist which can add/remove entries from
+	 * @return the existing encoded url blocklist, never null
+	 * @deprecated Use {@link #getEncodedUrlBlocklist()} instead
+	 */
+	@Deprecated
 	public Set<String> getEncodedUrlBlacklist() {
-		return encodedUrlBlacklist;
+		return getEncodedUrlBlocklist();
 	}
 
 	/**
-	 * Provides the existing decoded url blacklist which can add/remove entries from
+	 * Provides the existing decoded url blocklist which can add/remove entries from
+	 * @return the existing decoded url blocklist, never null
 	 *
-	 * @return the existing decoded url blacklist, never null
 	 */
 	public Set<String> getDecodedUrlBlacklist() {
-		return decodedUrlBlacklist;
+		return getDecodedUrlBlocklist();
 	}
+
+	/**
+	 * Strict {@link FirewalledRequest}.
+	 */
+	private class StrictFirewalledRequest extends FirewalledRequest {
+
+		StrictFirewalledRequest(HttpServletRequest request) {
+			super(request);
+		}
+
+		@Override
+		public long getDateHeader(String name) {
+			if (name != null) {
+				validateAllowedHeaderName(name);
+			}
+			return super.getDateHeader(name);
+		}
+
+		@Override
+		public int getIntHeader(String name) {
+			if (name != null) {
+				validateAllowedHeaderName(name);
+			}
+			return super.getIntHeader(name);
+		}
+
+		@Override
+		public String getHeader(String name) {
+			if (name != null) {
+				validateAllowedHeaderName(name);
+			}
+			String value = super.getHeader(name);
+			if (value != null) {
+				validateAllowedHeaderValue(value);
+			}
+			return value;
+		}
+
+		@Override
+		public Enumeration<String> getHeaders(String name) {
+			if (name != null) {
+				validateAllowedHeaderName(name);
+			}
+			Enumeration<String> headers = super.getHeaders(name);
+			return new Enumeration<String>() {
+
+				@Override
+				public boolean hasMoreElements() {
+					return headers.hasMoreElements();
+				}
+
+				@Override
+				public String nextElement() {
+					String value = headers.nextElement();
+					validateAllowedHeaderValue(value);
+					return value;
+				}
+
+			};
+		}
+
+		@Override
+		public Enumeration<String> getHeaderNames() {
+			Enumeration<String> names = super.getHeaderNames();
+			return new Enumeration<String>() {
+
+				@Override
+				public boolean hasMoreElements() {
+					return names.hasMoreElements();
+				}
+
+				@Override
+				public String nextElement() {
+					String headerNames = names.nextElement();
+					validateAllowedHeaderName(headerNames);
+					return headerNames;
+				}
+
+			};
+		}
+
+		@Override
+		public String getParameter(String name) {
+			if (name != null) {
+				validateAllowedParameterName(name);
+			}
+			String value = super.getParameter(name);
+			if (value != null) {
+				validateAllowedParameterValue(value);
+			}
+			return value;
+		}
+
+		@Override
+		public Map<String, String[]> getParameterMap() {
+			Map<String, String[]> parameterMap = super.getParameterMap();
+			for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
+				String name = entry.getKey();
+				String[] values = entry.getValue();
+				validateAllowedParameterName(name);
+				for (String value : values) {
+					validateAllowedParameterValue(value);
+				}
+			}
+			return parameterMap;
+		}
+
+		@Override
+		public Enumeration<String> getParameterNames() {
+			Enumeration<String> paramaterNames = super.getParameterNames();
+			return new Enumeration<String>() {
+
+				@Override
+				public boolean hasMoreElements() {
+					return paramaterNames.hasMoreElements();
+				}
+
+				@Override
+				public String nextElement() {
+					String name = paramaterNames.nextElement();
+					validateAllowedParameterName(name);
+					return name;
+				}
+
+			};
+		}
+
+		@Override
+		public String[] getParameterValues(String name) {
+			if (name != null) {
+				validateAllowedParameterName(name);
+			}
+			String[] values = super.getParameterValues(name);
+			if (values != null) {
+				for (String value : values) {
+					validateAllowedParameterValue(value);
+				}
+			}
+			return values;
+		}
+
+		private void validateAllowedHeaderName(String headerNames) {
+			if (!StrictHttpFirewall.this.allowedHeaderNames.test(headerNames)) {
+				throw new RequestRejectedException(
+						"The request was rejected because the header name \"" + headerNames + "\" is not allowed.");
+			}
+		}
+
+		private void validateAllowedHeaderValue(String value) {
+			if (!StrictHttpFirewall.this.allowedHeaderValues.test(value)) {
+				throw new RequestRejectedException(
+						"The request was rejected because the header value \"" + value + "\" is not allowed.");
+			}
+		}
+
+		private void validateAllowedParameterName(String name) {
+			if (!StrictHttpFirewall.this.allowedParameterNames.test(name)) {
+				throw new RequestRejectedException(
+						"The request was rejected because the parameter name \"" + name + "\" is not allowed.");
+			}
+		}
+
+		private void validateAllowedParameterValue(String value) {
+			if (!StrictHttpFirewall.this.allowedParameterValues.test(value)) {
+				throw new RequestRejectedException(
+						"The request was rejected because the parameter value \"" + value + "\" is not allowed.");
+			}
+		}
+
+		@Override
+		public void reset() {
+		}
+
+	};
+
 }

+ 448 - 130
web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2012-2019 the original author or authors.
+ * Copyright 2012-2021 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.
@@ -16,26 +16,27 @@
 
 package org.springframework.security.web.firewall;
 
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
-import static org.assertj.core.api.Assertions.fail;
-
 import java.util.Arrays;
 import java.util.List;
 
+import javax.servlet.http.HttpServletRequest;
+
 import org.junit.Test;
+
 import org.springframework.http.HttpMethod;
 import org.springframework.mock.web.MockHttpServletRequest;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+
 /**
  * @author Rob Winch
  * @author Eddú Meléndez
  */
 public class StrictHttpFirewallTests {
-	public String[] unnormalizedPaths = { "/..", "/./path/", "/path/path/.", "/path/path//.", "./path/../path//.",
-		"./path", ".//path", ".", "//path", "//path/path", "//path//path", "/path//path" };
 
+	public String[] unnormalizedPaths = { "/..", "/./path/", "/path/path/.", "/path/path//.", "./path/../path//.",
+			"./path", ".//path", ".", "//path", "//path/path", "//path//path", "/path//path" };
 
 	private StrictHttpFirewall firewall = new StrictHttpFirewall();
 
@@ -44,32 +45,32 @@ public class StrictHttpFirewallTests {
 	@Test
 	public void getFirewalledRequestWhenInvalidMethodThenThrowsRequestRejectedException() {
 		this.request.setMethod("INVALID");
-		assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request))
-				.isInstanceOf(RequestRejectedException.class);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
 	// blocks XST attacks
 	@Test
 	public void getFirewalledRequestWhenTraceMethodThenThrowsRequestRejectedException() {
 		this.request.setMethod(HttpMethod.TRACE.name());
-		assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request))
-				.isInstanceOf(RequestRejectedException.class);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
 	@Test
 	// blocks XST attack if request is forwarded to a Microsoft IIS web server
 	public void getFirewalledRequestWhenTrackMethodThenThrowsRequestRejectedException() {
 		this.request.setMethod("TRACK");
-		assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request))
-				.isInstanceOf(RequestRejectedException.class);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
 	@Test
 	// HTTP methods are case sensitive
 	public void getFirewalledRequestWhenLowercaseGetThenThrowsRequestRejectedException() {
 		this.request.setMethod("get");
-		assertThatThrownBy(() -> this.firewall.getFirewalledRequest(this.request))
-				.isInstanceOf(RequestRejectedException.class);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
 	@Test
@@ -77,8 +78,7 @@ public class StrictHttpFirewallTests {
 		List<String> allowedMethods = Arrays.asList("DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT");
 		for (String allowedMethod : allowedMethods) {
 			this.request = new MockHttpServletRequest(allowedMethod, "");
-			assertThatCode(() -> this.firewall.getFirewalledRequest(this.request))
-					.doesNotThrowAnyException();
+			this.firewall.getFirewalledRequest(this.request);
 		}
 	}
 
@@ -86,8 +86,7 @@ public class StrictHttpFirewallTests {
 	public void getFirewalledRequestWhenInvalidMethodAndAnyMethodThenNoException() {
 		this.firewall.setUnsafeAllowAnyHttpMethod(true);
 		this.request.setMethod("INVALID");
-		assertThatCode(() -> this.firewall.getFirewalledRequest(this.request))
-				.doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(this.request);
 	}
 
 	@Test
@@ -95,11 +94,8 @@ public class StrictHttpFirewallTests {
 		for (String path : this.unnormalizedPaths) {
 			this.request = new MockHttpServletRequest("GET", "");
 			this.request.setRequestURI(path);
-			try {
-				this.firewall.getFirewalledRequest(this.request);
-				fail(path + " is un-normalized");
-			} catch (RequestRejectedException expected) {
-			}
+			assertThatExceptionOfType(RequestRejectedException.class)
+					.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 		}
 	}
 
@@ -108,11 +104,8 @@ public class StrictHttpFirewallTests {
 		for (String path : this.unnormalizedPaths) {
 			this.request = new MockHttpServletRequest("GET", "");
 			this.request.setContextPath(path);
-			try {
-				this.firewall.getFirewalledRequest(this.request);
-				fail(path + " is un-normalized");
-			} catch (RequestRejectedException expected) {
-			}
+			assertThatExceptionOfType(RequestRejectedException.class)
+					.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 		}
 	}
 
@@ -121,11 +114,8 @@ public class StrictHttpFirewallTests {
 		for (String path : this.unnormalizedPaths) {
 			this.request = new MockHttpServletRequest("GET", "");
 			this.request.setServletPath(path);
-			try {
-				this.firewall.getFirewalledRequest(this.request);
-				fail(path + " is un-normalized");
-			} catch (RequestRejectedException expected) {
-			}
+			assertThatExceptionOfType(RequestRejectedException.class)
+					.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 		}
 	}
 
@@ -134,105 +124,99 @@ public class StrictHttpFirewallTests {
 		for (String path : this.unnormalizedPaths) {
 			this.request = new MockHttpServletRequest("GET", "");
 			this.request.setPathInfo(path);
-			try {
-				this.firewall.getFirewalledRequest(this.request);
-				fail(path + " is un-normalized");
-			} catch (RequestRejectedException expected) {
-			}
+			assertThatExceptionOfType(RequestRejectedException.class)
+					.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 		}
 	}
 
-	// --- ; ---
-
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenSemicolonInContextPathThenThrowsRequestRejectedException() {
 		this.request.setContextPath(";/context");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenSemicolonInServletPathThenThrowsRequestRejectedException() {
 		this.request.setServletPath("/spring;/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenSemicolonInPathInfoThenThrowsRequestRejectedException() {
 		this.request.setPathInfo("/path;/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenSemicolonInRequestUriThenThrowsRequestRejectedException() {
 		this.request.setRequestURI("/path;/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenEncodedSemicolonInContextPathThenThrowsRequestRejectedException() {
 		this.request.setContextPath("%3B/context");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenEncodedSemicolonInServletPathThenThrowsRequestRejectedException() {
 		this.request.setServletPath("/spring%3B/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenEncodedSemicolonInPathInfoThenThrowsRequestRejectedException() {
 		this.request.setPathInfo("/path%3B/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenEncodedSemicolonInRequestUriThenThrowsRequestRejectedException() {
 		this.request.setRequestURI("/path%3B/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenLowercaseEncodedSemicolonInContextPathThenThrowsRequestRejectedException() {
 		this.request.setContextPath("%3b/context");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenLowercaseEncodedSemicolonInServletPathThenThrowsRequestRejectedException() {
 		this.request.setServletPath("/spring%3b/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenLowercaseEncodedSemicolonInPathInfoThenThrowsRequestRejectedException() {
 		this.request.setPathInfo("/path%3b/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenLowercaseEncodedSemicolonInRequestUriThenThrowsRequestRejectedException() {
 		this.request.setRequestURI("/path%3b/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
 	@Test
 	public void getFirewalledRequestWhenSemicolonInContextPathAndAllowSemicolonThenNoException() {
 		this.firewall.setAllowSemicolon(true);
 		this.request.setContextPath(";/context");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -240,7 +224,6 @@ public class StrictHttpFirewallTests {
 	public void getFirewalledRequestWhenSemicolonInServletPathAndAllowSemicolonThenNoException() {
 		this.firewall.setAllowSemicolon(true);
 		this.request.setServletPath("/spring;/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -248,7 +231,6 @@ public class StrictHttpFirewallTests {
 	public void getFirewalledRequestWhenSemicolonInPathInfoAndAllowSemicolonThenNoException() {
 		this.firewall.setAllowSemicolon(true);
 		this.request.setPathInfo("/path;/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -256,7 +238,6 @@ public class StrictHttpFirewallTests {
 	public void getFirewalledRequestWhenSemicolonInRequestUriAndAllowSemicolonThenNoException() {
 		this.firewall.setAllowSemicolon(true);
 		this.request.setRequestURI("/path;/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -265,7 +246,6 @@ public class StrictHttpFirewallTests {
 		this.firewall.setAllowUrlEncodedPercent(true);
 		this.firewall.setAllowSemicolon(true);
 		this.request.setContextPath("%3B/context");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -274,7 +254,6 @@ public class StrictHttpFirewallTests {
 		this.firewall.setAllowUrlEncodedPercent(true);
 		this.firewall.setAllowSemicolon(true);
 		this.request.setServletPath("/spring%3B/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -283,7 +262,6 @@ public class StrictHttpFirewallTests {
 		this.firewall.setAllowUrlEncodedPercent(true);
 		this.firewall.setAllowSemicolon(true);
 		this.request.setPathInfo("/path%3B/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -291,7 +269,6 @@ public class StrictHttpFirewallTests {
 	public void getFirewalledRequestWhenEncodedSemicolonInRequestUriAndAllowSemicolonThenNoException() {
 		this.firewall.setAllowSemicolon(true);
 		this.request.setRequestURI("/path%3B/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -300,7 +277,6 @@ public class StrictHttpFirewallTests {
 		this.firewall.setAllowUrlEncodedPercent(true);
 		this.firewall.setAllowSemicolon(true);
 		this.request.setContextPath("%3b/context");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -309,7 +285,6 @@ public class StrictHttpFirewallTests {
 		this.firewall.setAllowUrlEncodedPercent(true);
 		this.firewall.setAllowSemicolon(true);
 		this.request.setServletPath("/spring%3b/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -318,7 +293,6 @@ public class StrictHttpFirewallTests {
 		this.firewall.setAllowUrlEncodedPercent(true);
 		this.firewall.setAllowSemicolon(true);
 		this.request.setPathInfo("/path%3b/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
@@ -326,38 +300,35 @@ public class StrictHttpFirewallTests {
 	public void getFirewalledRequestWhenLowercaseEncodedSemicolonInRequestUriAndAllowSemicolonThenNoException() {
 		this.firewall.setAllowSemicolon(true);
 		this.request.setRequestURI("/path%3b/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
-	// --- encoded . ---
-
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenEncodedPeriodInThenThrowsRequestRejectedException() {
 		this.request.setRequestURI("/%2E/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenLowercaseEncodedPeriodInThenThrowsRequestRejectedException() {
 		this.request.setRequestURI("/%2e/");
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
 	@Test
 	public void getFirewalledRequestWhenAllowEncodedPeriodAndEncodedPeriodInThenNoException() {
 		this.firewall.setAllowUrlEncodedPeriod(true);
 		this.request.setRequestURI("/%2E/");
-
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenExceedsLowerboundAsciiThenException() {
 		this.request.setRequestURI("/\u0019");
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
 	@Test
@@ -372,13 +343,46 @@ public class StrictHttpFirewallTests {
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
+	public void getFirewalledRequestWhenJapaneseCharacterThenNoException() {
+		this.request.setServletPath("/\u3042");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
+	@Test
 	public void getFirewalledRequestWhenExceedsUpperboundAsciiThenException() {
 		this.request.setRequestURI("/\u007f");
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	// --- from DefaultHttpFirewallTests ---
+	@Test
+	public void getFirewalledRequestWhenContainsNullThenException() {
+		this.request.setRequestURI("/\0");
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsEncodedNullThenException() {
+		this.request.setRequestURI("/something%00/");
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsLowercaseEncodedLineFeedThenException() {
+		this.request.setRequestURI("/something%0a/");
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsUppercaseEncodedLineFeedThenException() {
+		this.request.setRequestURI("/something%0A/");
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
 
 	@Test
 	public void getFirewalledRequestWhenContainsLineFeedThenException() {
@@ -394,6 +398,20 @@ public class StrictHttpFirewallTests {
 				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
+	@Test
+	public void getFirewalledRequestWhenContainsLowercaseEncodedCarriageReturnThenException() {
+		this.request.setRequestURI("/something%0d/");
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsUppercaseEncodedCarriageReturnThenException() {
+		this.request.setRequestURI("/something%0D/");
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
+
 	@Test
 	public void getFirewalledRequestWhenContainsCarriageReturnThenException() {
 		this.request.setRequestURI("/something\r/");
@@ -408,29 +426,119 @@ public class StrictHttpFirewallTests {
 				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
+	@Test
+	public void getFirewalledRequestWhenServletPathContainsLineSeparatorThenException() {
+		this.request.setServletPath("/something\u2028/");
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
+
+	@Test
+	public void getFirewalledRequestWhenServletPathContainsParagraphSeparatorThenException() {
+		this.request.setServletPath("/something\u2029/");
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsLowercaseEncodedLineFeedAndAllowedThenNoException() {
+		this.firewall.setAllowUrlEncodedLineFeed(true);
+		this.request.setRequestURI("/something%0a/");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsUppercaseEncodedLineFeedAndAllowedThenNoException() {
+		this.firewall.setAllowUrlEncodedLineFeed(true);
+		this.request.setRequestURI("/something%0A/");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsLineFeedAndAllowedThenException() {
+		this.firewall.setAllowUrlEncodedLineFeed(true);
+		this.request.setRequestURI("/something\n/");
+		// Expected an error because the line feed is decoded in an encoded part of the
+		// URL
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
+
+	@Test
+	public void getFirewalledRequestWhenServletPathContainsLineFeedAndAllowedThenNoException() {
+		this.firewall.setAllowUrlEncodedLineFeed(true);
+		this.request.setServletPath("/something\n/");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsLowercaseEncodedCarriageReturnAndAllowedThenNoException() {
+		this.firewall.setAllowUrlEncodedCarriageReturn(true);
+		this.request.setRequestURI("/something%0d/");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsUppercaseEncodedCarriageReturnAndAllowedThenNoException() {
+		this.firewall.setAllowUrlEncodedCarriageReturn(true);
+		this.request.setRequestURI("/something%0D/");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenContainsCarriageReturnAndAllowedThenNoException() {
+		this.firewall.setAllowUrlEncodedCarriageReturn(true);
+		this.request.setRequestURI("/something\r/");
+		// Expected an error because the carriage return is decoded in an encoded part of
+		// the URL
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
+
+	@Test
+	public void getFirewalledRequestWhenServletPathContainsCarriageReturnAndAllowedThenNoException() {
+		this.firewall.setAllowUrlEncodedCarriageReturn(true);
+		this.request.setServletPath("/something\r/");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenServletPathContainsLineSeparatorAndAllowedThenNoException() {
+		this.firewall.setAllowUrlEncodedLineSeparator(true);
+		this.request.setServletPath("/something\u2028/");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenServletPathContainsParagraphSeparatorAndAllowedThenNoException() {
+		this.firewall.setAllowUrlEncodedParagraphSeparator(true);
+		this.request.setServletPath("/something\u2029/");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
 	/**
-	 * On WebSphere 8.5 a URL like /context-root/a/b;%2f1/c can bypass a rule on
-	 * /a/b/c because the pathInfo is /a/b;/1/c which ends up being /a/b/1/c
-	 * while Spring MVC will strip the ; content from requestURI before the path
-	 * is URL decoded.
+	 * On WebSphere 8.5 a URL like /context-root/a/b;%2f1/c can bypass a rule on /a/b/c
+	 * because the pathInfo is /a/b;/1/c which ends up being /a/b/1/c while Spring MVC
+	 * will strip the ; content from requestURI before the path is URL decoded.
 	 */
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenLowercaseEncodedPathThenException() {
 		this.request.setRequestURI("/context-root/a/b;%2f1/c");
 		this.request.setContextPath("/context-root");
 		this.request.setServletPath("");
 		this.request.setPathInfo("/a/b;/1/c"); // URL decoded requestURI
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenUppercaseEncodedPathThenException() {
 		this.request.setRequestURI("/context-root/a/b;%2F1/c");
 		this.request.setContextPath("/context-root");
 		this.request.setServletPath("");
 		this.request.setPathInfo("/a/b;/1/c"); // URL decoded requestURI
-
-		this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
 	}
 
 	@Test
@@ -442,7 +550,6 @@ public class StrictHttpFirewallTests {
 		request.setContextPath("/context-root");
 		request.setServletPath("");
 		request.setPathInfo("/a/b;/1/c"); // URL decoded requestURI
-
 		this.firewall.getFirewalledRequest(request);
 	}
 
@@ -455,7 +562,6 @@ public class StrictHttpFirewallTests {
 		request.setContextPath("/context-root");
 		request.setServletPath("");
 		request.setPathInfo("/a/b;/1/c"); // URL decoded requestURI
-
 		this.firewall.getFirewalledRequest(request);
 	}
 
@@ -468,7 +574,7 @@ public class StrictHttpFirewallTests {
 		request.setContextPath("/context-root");
 		request.setServletPath("");
 		request.setPathInfo("/a/b//c");
-		assertThatCode(() -> this.firewall.getFirewalledRequest(request)).doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(request);
 	}
 
 	@Test
@@ -480,7 +586,7 @@ public class StrictHttpFirewallTests {
 		request.setContextPath("/context-root");
 		request.setServletPath("");
 		request.setPathInfo("/a/b//c");
-		assertThatCode(() -> this.firewall.getFirewalledRequest(request)).doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(request);
 	}
 
 	@Test
@@ -492,7 +598,7 @@ public class StrictHttpFirewallTests {
 		request.setContextPath("/context-root");
 		request.setServletPath("");
 		request.setPathInfo("/a/b//c");
-		assertThatCode(() -> this.firewall.getFirewalledRequest(request)).doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(request);
 	}
 
 	@Test
@@ -504,7 +610,7 @@ public class StrictHttpFirewallTests {
 		request.setContextPath("/context-root");
 		request.setServletPath("");
 		request.setPathInfo("/a/b//c");
-		assertThatCode(() -> this.firewall.getFirewalledRequest(request)).doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(request);
 	}
 
 	@Test
@@ -513,7 +619,7 @@ public class StrictHttpFirewallTests {
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
 		request.setRequestURI("/context-root/a/b%2F%2Fc");
 		this.firewall.getEncodedUrlBlacklist().removeAll(Arrays.asList("%2F%2F"));
-		assertThatCode(() -> this.firewall.getFirewalledRequest(request)).doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(request);
 	}
 
 	@Test
@@ -522,7 +628,7 @@ public class StrictHttpFirewallTests {
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
 		request.setRequestURI("/context-root/a/b%2f%2fc");
 		this.firewall.getEncodedUrlBlacklist().removeAll(Arrays.asList("%2f%2f"));
-		assertThatCode(() -> this.firewall.getFirewalledRequest(request)).doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(request);
 	}
 
 	@Test
@@ -531,7 +637,7 @@ public class StrictHttpFirewallTests {
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
 		request.setRequestURI("/context-root/a/b%2f%2Fc");
 		this.firewall.getEncodedUrlBlacklist().removeAll(Arrays.asList("%2f%2F"));
-		assertThatCode(() -> this.firewall.getFirewalledRequest(request)).doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(request);
 	}
 
 	@Test
@@ -540,7 +646,7 @@ public class StrictHttpFirewallTests {
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
 		request.setRequestURI("/context-root/a/b%2F%2fc");
 		this.firewall.getEncodedUrlBlacklist().removeAll(Arrays.asList("%2F%2f"));
-		assertThatCode(() -> this.firewall.getFirewalledRequest(request)).doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(request);
 	}
 
 	@Test
@@ -548,22 +654,234 @@ public class StrictHttpFirewallTests {
 		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
 		request.setPathInfo("/a/b//c");
 		this.firewall.getDecodedUrlBlacklist().removeAll(Arrays.asList("//"));
-		assertThatCode(() -> this.firewall.getFirewalledRequest(request)).doesNotThrowAnyException();
+		this.firewall.getFirewalledRequest(request);
+	}
+
+	// blocklist
+	@Test
+	public void getFirewalledRequestWhenRemoveFromUpperCaseEncodedUrlBlocklistThenNoException() {
+		this.firewall.setAllowUrlEncodedSlash(true);
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
+		request.setRequestURI("/context-root/a/b%2F%2Fc");
+		this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2F%2F"));
+		this.firewall.getFirewalledRequest(request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenRemoveFromLowerCaseEncodedUrlBlocklistThenNoException() {
+		this.firewall.setAllowUrlEncodedSlash(true);
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
+		request.setRequestURI("/context-root/a/b%2f%2fc");
+		this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2f%2f"));
+		this.firewall.getFirewalledRequest(request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenRemoveFromLowerCaseAndUpperCaseEncodedUrlBlocklistThenNoException() {
+		this.firewall.setAllowUrlEncodedSlash(true);
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
+		request.setRequestURI("/context-root/a/b%2f%2Fc");
+		this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2f%2F"));
+		this.firewall.getFirewalledRequest(request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenRemoveFromUpperCaseAndLowerCaseEncodedUrlBlocklistThenNoException() {
+		this.firewall.setAllowUrlEncodedSlash(true);
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
+		request.setRequestURI("/context-root/a/b%2F%2fc");
+		this.firewall.getEncodedUrlBlocklist().removeAll(Arrays.asList("%2F%2f"));
+		this.firewall.getFirewalledRequest(request);
+	}
+
+	@Test
+	public void getFirewalledRequestWhenRemoveFromDecodedUrlBlocklistThenNoException() {
+		MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
+		request.setPathInfo("/a/b//c");
+		this.firewall.getDecodedUrlBlocklist().removeAll(Arrays.asList("//"));
+		this.firewall.getFirewalledRequest(request);
 	}
 
 	@Test
 	public void getFirewalledRequestWhenTrustedDomainThenNoException() {
 		this.request.addHeader("Host", "example.org");
-		this.firewall.setAllowedHostnames(hostname -> hostname.equals("example.org"));
-
-		assertThatCode(() -> this.firewall.getFirewalledRequest(this.request)).doesNotThrowAnyException();
+		this.firewall.setAllowedHostnames((hostname) -> hostname.equals("example.org"));
+		this.firewall.getFirewalledRequest(this.request);
 	}
 
-	@Test(expected = RequestRejectedException.class)
+	@Test
 	public void getFirewalledRequestWhenUntrustedDomainThenException() {
 		this.request.addHeader("Host", "example.org");
-		this.firewall.setAllowedHostnames(hostname -> hostname.equals("myexample.org"));
+		this.firewall.setAllowedHostnames((hostname) -> hostname.equals("myexample.org"));
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> this.firewall.getFirewalledRequest(this.request));
+	}
 
-		this.firewall.getFirewalledRequest(this.request);
+	@Test
+	public void getFirewalledRequestGetHeaderWhenNotAllowedHeaderNameThenException() {
+		this.firewall.setAllowedHeaderNames((name) -> !name.equals("bad name"));
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("bad name"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetHeaderWhenNotAllowedHeaderValueThenException() {
+		this.request.addHeader("good name", "bad value");
+		this.firewall.setAllowedHeaderValues((value) -> !value.equals("bad value"));
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("good name"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetDateHeaderWhenControlCharacterInHeaderNameThenException() {
+		this.request.addHeader("Bad\0Name", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getDateHeader("Bad\0Name"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetIntHeaderWhenControlCharacterInHeaderNameThenException() {
+		this.request.addHeader("Bad\0Name", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getIntHeader("Bad\0Name"));
 	}
+
+	@Test
+	public void getFirewalledRequestGetHeaderWhenControlCharacterInHeaderNameThenException() {
+		this.request.addHeader("Bad\0Name", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("Bad\0Name"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetHeaderWhenUndefinedCharacterInHeaderNameThenException() {
+		this.request.addHeader("Bad\uFFFEName", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("Bad\uFFFEName"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetHeadersWhenControlCharacterInHeaderNameThenException() {
+		this.request.addHeader("Bad\0Name", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeaders("Bad\0Name"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetHeaderNamesWhenControlCharacterInHeaderNameThenException() {
+		this.request.addHeader("Bad\0Name", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> request.getHeaderNames().nextElement());
+	}
+
+	@Test
+	public void getFirewalledRequestGetHeaderWhenControlCharacterInHeaderValueThenException() {
+		this.request.addHeader("Something", "bad\0value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("Something"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetHeaderWhenUndefinedCharacterInHeaderValueThenException() {
+		this.request.addHeader("Something", "bad\uFFFEvalue");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getHeader("Something"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetHeadersWhenControlCharacterInHeaderValueThenException() {
+		this.request.addHeader("Something", "bad\0value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> request.getHeaders("Something").nextElement());
+	}
+
+	@Test
+	public void getFirewalledRequestGetParameterWhenControlCharacterInParameterNameThenException() {
+		this.request.addParameter("Bad\0Name", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(() -> request.getParameter("Bad\0Name"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetParameterMapWhenControlCharacterInParameterNameThenException() {
+		this.request.addParameter("Bad\0Name", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(request::getParameterMap);
+	}
+
+	@Test
+	public void getFirewalledRequestGetParameterNamesWhenControlCharacterInParameterNameThenException() {
+		this.request.addParameter("Bad\0Name", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(request.getParameterNames()::nextElement);
+	}
+
+	@Test
+	public void getFirewalledRequestGetParameterNamesWhenUndefinedCharacterInParameterNameThenException() {
+		this.request.addParameter("Bad\uFFFEName", "some value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class).isThrownBy(request.getParameterNames()::nextElement);
+	}
+
+	@Test
+	public void getFirewalledRequestGetParameterValuesWhenNotAllowedInParameterValueThenException() {
+		this.firewall.setAllowedParameterValues((value) -> !value.equals("bad value"));
+		this.request.addParameter("Something", "bad value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> request.getParameterValues("Something"));
+	}
+
+	@Test
+	public void getFirewalledRequestGetParameterValuesWhenNotAllowedInParameterNameThenException() {
+		this.firewall.setAllowedParameterNames((value) -> !value.equals("bad name"));
+		this.request.addParameter("bad name", "good value");
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(RequestRejectedException.class)
+				.isThrownBy(() -> request.getParameterValues("bad name"));
+	}
+
+	// gh-9598
+	@Test
+	public void getFirewalledRequestGetParameterWhenNameIsNullThenIllegalArgumentException() {
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> request.getParameter(null));
+	}
+
+	// gh-9598
+	@Test
+	public void getFirewalledRequestGetParameterValuesWhenNameIsNullThenIllegalArgumentException() {
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> request.getParameterValues(null));
+	}
+
+	// gh-9598
+	@Test
+	public void getFirewalledRequestGetHeaderWhenNameIsNullThenNull() {
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThat(request.getHeader(null)).isNull();
+	}
+
+	// gh-9598
+	@Test
+	public void getFirewalledRequestGetHeadersWhenNameIsNullThenEmptyEnumeration() {
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThat(request.getHeaders(null).hasMoreElements()).isFalse();
+	}
+
+	// gh-9598
+	@Test
+	public void getFirewalledRequestGetIntHeaderWhenNameIsNullThenNegativeOne() {
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThat(request.getIntHeader(null)).isEqualTo(-1);
+	}
+
+	@Test
+	public void getFirewalledRequestGetDateHeaderWhenNameIsNullThenNegativeOne() {
+		HttpServletRequest request = this.firewall.getFirewalledRequest(this.request);
+		assertThat(request.getDateHeader(null)).isEqualTo(-1);
+	}
+
 }