瀏覽代碼

StrictHttpFirewall allows CJKV characters

Closes gh-11264
Rob Winch 3 年之前
父節點
當前提交
29b2b7a977

+ 76 - 3
web/src/main/java/org/springframework/security/web/firewall/StrictHttpFirewall.java

@@ -107,6 +107,15 @@ public class StrictHttpFirewall implements HttpFirewall {
 
 	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<>();
@@ -135,10 +144,14 @@ public class StrictHttpFirewall implements HttpFirewall {
 		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);
 	}
 
 	/**
@@ -345,6 +358,69 @@ public class StrictHttpFirewall implements HttpFirewall {
 		}
 	}
 
+	/**
+	 * 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
@@ -432,9 +508,6 @@ public class StrictHttpFirewall implements HttpFirewall {
 			throw new RequestRejectedException("The request was rejected because the URL was not normalized.");
 		}
 		rejectNonPrintableAsciiCharactersInFieldName(request.getRequestURI(), "requestURI");
-		rejectNonPrintableAsciiCharactersInFieldName(request.getServletPath(), "servletPath");
-		rejectNonPrintableAsciiCharactersInFieldName(request.getPathInfo(), "pathInfo");
-		rejectNonPrintableAsciiCharactersInFieldName(request.getContextPath(), "contextPath");
 		return new StrictFirewalledRequest(request);
 	}
 

+ 124 - 0
web/src/test/java/org/springframework/security/web/firewall/StrictHttpFirewallTests.java

@@ -343,6 +343,12 @@ public class StrictHttpFirewallTests {
 		this.firewall.getFirewalledRequest(this.request);
 	}
 
+	@Test
+	public void getFirewalledRequestWhenJapaneseCharacterThenNoException() {
+		this.request.setServletPath("/\u3042");
+		this.firewall.getFirewalledRequest(this.request);
+	}
+
 	@Test
 	public void getFirewalledRequestWhenExceedsUpperboundAsciiThenException() {
 		this.request.setRequestURI("/\u007f");
@@ -364,6 +370,20 @@ public class StrictHttpFirewallTests {
 				.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() {
 		this.request.setRequestURI("/something\n/");
@@ -378,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/");
@@ -392,6 +426,96 @@ 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