瀏覽代碼

Add reactive support for Content-Security-Policy security header

Vedran Pavic 7 年之前
父節點
當前提交
10621a0f2c

+ 47 - 1
config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java

@@ -98,6 +98,7 @@ import org.springframework.security.web.server.csrf.CsrfWebFilter;
 import org.springframework.security.web.server.csrf.ServerCsrfTokenRepository;
 import org.springframework.security.web.server.header.CacheControlServerHttpHeadersWriter;
 import org.springframework.security.web.server.header.CompositeServerHttpHeadersWriter;
+import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter;
 import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter;
 import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter;
 import org.springframework.security.web.server.header.HttpHeaderWriterWebFilter;
@@ -1664,6 +1665,8 @@ public class ServerHttpSecurity {
 
 		private FeaturePolicyServerHttpHeadersWriter featurePolicy = new FeaturePolicyServerHttpHeadersWriter();
 
+		private ContentSecurityPolicyServerHttpHeadersWriter contentSecurityPolicy = new ContentSecurityPolicyServerHttpHeadersWriter();
+
 		/**
 		 * Allows method chaining to continue configuring the {@link ServerHttpSecurity}
 		 * @return the {@link ServerHttpSecurity} to continue configuring
@@ -1727,6 +1730,15 @@ public class ServerHttpSecurity {
 			return new XssProtectionSpec();
 		}
 
+		/**
+		 * Configures {@code Content-Security-Policy} response header.
+		 * @param policyDirectives the policy directive(s)
+		 * @return the {@link ContentSecurityPolicySpec} to configure
+		 */
+		public ContentSecurityPolicySpec contentSecurityPolicy(String policyDirectives) {
+			return new ContentSecurityPolicySpec(policyDirectives);
+		}
+
 		/**
 		 * Configures {@code Feature-Policy} response header.
 		 * @param policyDirectives the policy directive(s)
@@ -1868,6 +1880,40 @@ public class ServerHttpSecurity {
 			private XssProtectionSpec() {}
 		}
 
+		/**
+		 * Configures {@code Content-Security-Policy} response header.
+		 *
+		 * @see #contentSecurityPolicy(String)
+		 * @since 5.1
+		 */
+		public class ContentSecurityPolicySpec {
+
+			/**
+			 * Whether to include the {@code Content-Security-Policy-Report-Only} header in
+			 * the response. Otherwise, defaults to the {@code Content-Security-Policy} header.
+			 * @param reportOnly whether to only report policy violations
+			 * @return the {@link HeaderSpec} to continue configuring
+			 */
+			public HeaderSpec reportOnly(boolean reportOnly) {
+				HeaderSpec.this.contentSecurityPolicy.setReportOnly(reportOnly);
+				return HeaderSpec.this;
+			}
+
+			/**
+			 * Allows method chaining to continue configuring the
+			 * {@link ServerHttpSecurity}.
+			 * @return the {@link HeaderSpec} to continue configuring
+			 */
+			public HeaderSpec and() {
+				return HeaderSpec.this;
+			}
+
+			private ContentSecurityPolicySpec(String policyDirectives) {
+				HeaderSpec.this.contentSecurityPolicy.setPolicyDirectives(policyDirectives);
+			}
+
+		}
+
 		/**
 		 * Configures {@code Feature-Policy} response header.
 		 *
@@ -1894,7 +1940,7 @@ public class ServerHttpSecurity {
 		private HeaderSpec() {
 			this.writers = new ArrayList<>(
 					Arrays.asList(this.cacheControl, this.contentTypeOptions, this.hsts,
-							this.frameOptions, this.xss, this.featurePolicy));
+							this.frameOptions, this.xss, this.featurePolicy, this.contentSecurityPolicy));
 		}
 
 	}

+ 13 - 0
config/src/test/java/org/springframework/security/config/web/server/HeaderSpecTests.java

@@ -27,6 +27,7 @@ import org.junit.Test;
 
 import org.springframework.http.HttpHeaders;
 import org.springframework.security.test.web.reactive.server.WebTestClientBuilder;
+import org.springframework.security.web.server.header.ContentSecurityPolicyServerHttpHeadersWriter;
 import org.springframework.security.web.server.header.ContentTypeOptionsServerHttpHeadersWriter;
 import org.springframework.security.web.server.header.FeaturePolicyServerHttpHeadersWriter;
 import org.springframework.security.web.server.header.StrictTransportSecurityServerHttpHeadersWriter;
@@ -153,11 +154,23 @@ public class HeaderSpecTests {
 		String policyDirectives = "Feature-Policy";
 		this.expectedHeaders.add(FeaturePolicyServerHttpHeadersWriter.FEATURE_POLICY,
 				policyDirectives);
+
 		this.headers.featurePolicy(policyDirectives);
 
 		assertHeaders();
 	}
 
+	@Test
+	public void headersWhenContentSecurityPolicyEnabledThenFeaturePolicyWritten() {
+		String policyDirectives = "default-src 'self'";
+		this.expectedHeaders.add(ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY,
+				policyDirectives);
+
+		this.headers.contentSecurityPolicy(policyDirectives);
+
+		assertHeaders();
+	}
+
 	private void expectHeaderNamesNotPresent(String... headerNames) {
 		for(String headerName : headerNames) {
 			this.expectedHeaders.remove(headerName);

+ 88 - 0
web/src/main/java/org/springframework/security/web/server/header/ContentSecurityPolicyServerHttpHeadersWriter.java

@@ -0,0 +1,88 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.header;
+
+import reactor.core.publisher.Mono;
+
+import org.springframework.util.Assert;
+import org.springframework.web.server.ServerWebExchange;
+
+/**
+ * Writes the {@code Contet-Security-Policy} response header with configured policy
+ * directives.
+ *
+ * @author Vedran Pavic
+ * @since 5.1
+ */
+public final class ContentSecurityPolicyServerHttpHeadersWriter
+		implements ServerHttpHeadersWriter {
+
+	public static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
+
+	public static final String CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only";
+
+	private String policyDirectives;
+
+	private boolean reportOnly;
+
+	private ServerHttpHeadersWriter delegate;
+
+	@Override
+	public Mono<Void> writeHttpHeaders(ServerWebExchange exchange) {
+		return (this.delegate != null) ? this.delegate.writeHttpHeaders(exchange)
+				: Mono.empty();
+	}
+
+	/**
+	 * Set the policy directive(s) to be used in the response header.
+	 * @param policyDirectives the policy directive(s)
+	 * @throws IllegalArgumentException if policyDirectives is {@code null} or empty
+	 */
+	public void setPolicyDirectives(String policyDirectives) {
+		Assert.hasLength(policyDirectives, "policyDirectives must not be null or empty");
+		this.policyDirectives = policyDirectives;
+		this.delegate = createDelegate();
+	}
+
+	/**
+	 * Set whether to include the {@code Content-Security-Policy-Report-Only} header in
+	 * the response. Otherwise, defaults to the {@code Content-Security-Policy} header.
+	 * @param reportOnly whether to only report policy violations
+	 */
+	public void setReportOnly(boolean reportOnly) {
+		this.reportOnly = reportOnly;
+		this.delegate = createDelegate();
+	}
+
+	private ServerHttpHeadersWriter createDelegate() {
+		if (this.policyDirectives != null) {
+			// @formatter:off
+		return StaticServerHttpHeadersWriter.builder()
+				.header(resolveHeader(this.reportOnly), this.policyDirectives)
+				.build();
+		// @formatter:on
+		}
+		else {
+			return null;
+		}
+	}
+
+	private static String resolveHeader(boolean reportOnly) {
+		return reportOnly ? CONTENT_SECURITY_POLICY_REPORT_ONLY : CONTENT_SECURITY_POLICY;
+	}
+
+}

+ 105 - 0
web/src/test/java/org/springframework/security/web/server/header/ContentSecurityPolicyServerHttpHeadersWriterTests.java

@@ -0,0 +1,105 @@
+/*
+ * Copyright 2002-2018 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
+ *
+ *      http://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.header;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
+import org.springframework.mock.web.server.MockServerWebExchange;
+import org.springframework.web.server.ServerWebExchange;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Tests for {@link ContentSecurityPolicyServerHttpHeadersWriter}.
+ *
+ * @author Vedran Pavic
+ */
+public class ContentSecurityPolicyServerHttpHeadersWriterTests {
+
+	private static final String DEFAULT_POLICY_DIRECTIVES = "default-src 'self'";
+
+	private ServerWebExchange exchange;
+
+	private ContentSecurityPolicyServerHttpHeadersWriter writer;
+
+	@Before
+	public void setup() {
+		this.exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
+		this.writer = new ContentSecurityPolicyServerHttpHeadersWriter();
+	}
+
+	@Test
+	public void writeHeadersWhenUsingDefaultsThenDoesNotWrite() {
+		this.writer.writeHttpHeaders(this.exchange);
+
+		HttpHeaders headers = this.exchange.getResponse().getHeaders();
+		assertThat(headers).isEmpty();
+	}
+
+	@Test
+	public void writeHeadersWhenUsingPolicyThenWritesPolicy() {
+		this.writer.setPolicyDirectives(DEFAULT_POLICY_DIRECTIVES);
+		this.writer.writeHttpHeaders(this.exchange);
+
+		HttpHeaders headers = this.exchange.getResponse().getHeaders();
+		assertThat(headers).hasSize(1);
+		assertThat(headers.get(
+				ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY))
+						.containsOnly(DEFAULT_POLICY_DIRECTIVES);
+	}
+
+	@Test
+	public void writeHeadersWhenReportPolicyThenWritesReportPolicy() {
+		this.writer.setPolicyDirectives(DEFAULT_POLICY_DIRECTIVES);
+		this.writer.setReportOnly(true);
+		this.writer.writeHttpHeaders(this.exchange);
+
+		HttpHeaders headers = this.exchange.getResponse().getHeaders();
+		assertThat(headers).hasSize(1);
+		assertThat(headers.get(
+				ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY_REPORT_ONLY))
+						.containsOnly(DEFAULT_POLICY_DIRECTIVES);
+	}
+
+	@Test
+	public void writeHeadersWhenOnlyReportOnlySetThenDoesNotWrite() {
+		this.writer.setReportOnly(true);
+		this.writer.writeHttpHeaders(this.exchange);
+
+		HttpHeaders headers = this.exchange.getResponse().getHeaders();
+		assertThat(headers).isEmpty();
+	}
+
+	@Test
+	public void writeHeadersWhenAlreadyWrittenThenWritesHeader() {
+		String headerValue = "default-src https: 'self'";
+		this.exchange.getResponse().getHeaders().set(
+				ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY,
+				headerValue);
+		this.writer.writeHttpHeaders(this.exchange);
+
+		HttpHeaders headers = this.exchange.getResponse().getHeaders();
+		assertThat(headers).hasSize(1);
+		assertThat(headers.get(
+				ContentSecurityPolicyServerHttpHeadersWriter.CONTENT_SECURITY_POLICY))
+						.containsOnly(headerValue);
+	}
+
+}