From d89cf6db29b840cfec9e79daab671129f6aee11f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 28 Aug 2013 12:35:40 -0500 Subject: [PATCH] SEC-2283: Update headers documentation and tests --- .../annotation/web/builders/HttpSecurity.java | 87 +++++++++- .../web/configurers/HeadersConfigurer.java | 121 +++++++++++-- .../configurers/HeadersConfigurerTests.groovy | 162 ++++++++++++++++++ .../manual/src/docbook/appendix-namespace.xml | 10 +- docs/manual/src/docbook/headers.xml | 74 +++++++- 5 files changed, 434 insertions(+), 20 deletions(-) create mode 100644 config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index 8969b873c6..75295dad11 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -239,6 +239,75 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder()); } + /** + * Adds the Security headers to the response. This is activated by default + * when using {@link WebSecurityConfigurerAdapter}'s default constructor. + * Only invoking the {@link #headers()} without invoking additional methods + * on it, or accepting the default provided by + * {@link WebSecurityConfigurerAdapter}, is the equivalent of: + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     * 	@Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .headers()
+     *                 .contentTypeOptions();
+     *                 .xssProtection()
+     *                 .cacheControl()
+     *                 .httpStrictTransportSecurity()
+     *                 .frameOptions()
+     *                 .and()
+     *             ...;
+     *     }
+     * }
+     * 
+ * + * You can disable the headers using the following: + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     * 	@Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .headers().disable()
+     *             ...;
+     *     }
+     * }
+     * 
+ * + * You can enable only a few of the headers by invoking the appropriate + * methods on {@link #headers()} result. For example, the following will + * enable {@link HeadersConfigurer#cacheControl()} and + * {@link HeadersConfigurer#frameOptions()} only. + * + *
+     * @Configuration
+     * @EnableWebSecurity
+     * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+     *
+     * 	@Override
+     *     protected void configure(HttpSecurity http) throws Exception {
+     *         http
+     *             .headers()
+     *                 .cacheControl()
+     *                 .frameOptions()
+     *                 .and()
+     *             ...;
+     *     }
+     * }
+     * 
+ * + * @return + * @throws Exception + * @see {@link HeadersConfigurer} + */ public HeadersConfigurer headers() throws Exception { return getOrApply(new HeadersConfigurer()); } @@ -664,7 +733,23 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder + * @Configuration + * @EnableWebSecurity + * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter { + * + * @Override + * protected void configure(HttpSecurity http) throws Exception { + * http + * .csrf().disable() + * ...; + * } + * } + * * * @return the {@link ServletApiConfigurer} for further customizations * @throws Exception diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java index 061ce79265..0635c23902 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/HeadersConfigurer.java @@ -20,6 +20,7 @@ import java.util.List; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.header.HeaderWriter; import org.springframework.security.web.header.HeaderWriterFilter; import org.springframework.security.web.header.writers.CacheControlHeadersWriter; @@ -30,15 +31,80 @@ import org.springframework.security.web.header.writers.frameoptions.XFrameOption import org.springframework.util.Assert; /** + * Adds the Security headers to the response. This is activated by default when + * using {@link WebSecurityConfigurerAdapter}'s default constructor. Only + * invoking the {@link #headers()} without invoking additional methods on it, or + * accepting the default provided by {@link WebSecurityConfigurerAdapter}, is + * the equivalent of: + * + *
+ * @Configuration
+ * @EnableWebSecurity
+ * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+ *
+ * 	@Override
+ *     protected void configure(HttpSecurity http) throws Exception {
+ *         http
+ *             .headers()
+ *                 .contentTypeOptions();
+ *                 .xssProtection()
+ *                 .cacheControl()
+ *                 .httpStrictTransportSecurity()
+ *                 .frameOptions()
+ *                 .and()
+ *             ...;
+ *     }
+ * }
+ * 
+ * + * You can disable the headers using the following: + * + *
+ * @Configuration
+ * @EnableWebSecurity
+ * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+ *
+ * 	@Override
+ *     protected void configure(HttpSecurity http) throws Exception {
+ *         http
+ *             .headers().disable()
+ *             ...;
+ *     }
+ * }
+ * 
+ * + * You can enable only a few of the headers by invoking the appropriate methods + * on {@link #headers()} result. For example, the following will enable + * {@link HeadersConfigurer#cacheControl()} and + * {@link HeadersConfigurer#frameOptions()} only. + * + *
+ * @Configuration
+ * @EnableWebSecurity
+ * public class CsrfSecurityConfig extends WebSecurityConfigurerAdapter {
+ *
+ * 	@Override
+ *     protected void configure(HttpSecurity http) throws Exception {
+ *         http
+ *             .headers()
+ *                 .cacheControl()
+ *                 .frameOptions()
+ *                 .and()
+ *             ...;
+ *     }
+ * }
+ * 
+ * * @author Rob Winch * @since 3.2 - * @see RememberMeConfigurer */ -public final class HeadersConfigurer> extends AbstractHttpConfigurer,H> { +public final class HeadersConfigurer> extends + AbstractHttpConfigurer, H> { private List headerWriters = new ArrayList(); /** * Creates a new instance + * * @see HttpSecurity#headers() */ public HeadersConfigurer() { @@ -46,7 +112,9 @@ public final class HeadersConfigurer> extends A /** * Adds a {@link HeaderWriter} instance - * @param headerWriter the {@link HeaderWriter} instance to add + * + * @param headerWriter + * the {@link HeaderWriter} instance to add * @return the {@link HeadersConfigurer} for additional customizations */ public HeadersConfigurer addHeaderWriter(HeaderWriter headerWriter) { @@ -56,7 +124,13 @@ public final class HeadersConfigurer> extends A } /** - * Adds {@link XContentTypeOptionsHeaderWriter} + * Adds {@link XContentTypeOptionsHeaderWriter} which inserts the X-Content-Type-Options: + * + *
+     * X-Content-Type-Options: nosniff
+     * 
* * @return the {@link HeadersConfigurer} for additional customizations */ @@ -65,8 +139,11 @@ public final class HeadersConfigurer> extends A } /** - * Adds {@link XXssProtectionHeaderWriter}. Note this is not comprehensive - * XSS protection! + * Note this is not comprehensive XSS protection! + * + * Adds {@link XXssProtectionHeaderWriter} which adds the X-XSS-Protection header * * @return the {@link HeadersConfigurer} for additional customizations */ @@ -75,7 +152,12 @@ public final class HeadersConfigurer> extends A } /** - * Adds {@link CacheControlHeadersWriter}. + * Adds {@link CacheControlHeadersWriter}. Specifically it adds the + * following headers: + *
    + *
  • Cache-Control: no-cache, no-store, max-age=0, must-revalidate
  • + *
  • Pragma: no-cache
  • + *
* * @return the {@link HeadersConfigurer} for additional customizations */ @@ -84,7 +166,15 @@ public final class HeadersConfigurer> extends A } /** - * Adds {@link HstsHeaderWriter}. + * Adds {@link HstsHeaderWriter} which provides support for HTTP Strict Transport Security + * (HSTS). + * + *

+ * For additional configuration options, use + * {@link #addHeaderWriter(HeaderWriter)} and {@link HstsHeaderWriter} + * directly. + *

* * @return the {@link HeadersConfigurer} for additional customizations */ @@ -93,7 +183,10 @@ public final class HeadersConfigurer> extends A } /** - * Adds {@link XFrameOptionsHeaderWriter} with all the default settings. + * Adds {@link XFrameOptionsHeaderWriter} with all the default settings. For + * additional configuration options, use + * {@link #addHeaderWriter(HeaderWriter)} and + * {@link XFrameOptionsHeaderWriter} directly. * * @return the {@link HeadersConfigurer} for additional customizations */ @@ -109,20 +202,24 @@ public final class HeadersConfigurer> extends A /** * Creates the {@link HeaderWriter} + * * @return the {@link HeaderWriter} */ private HeaderWriterFilter createHeaderWriterFilter() { - HeaderWriterFilter headersFilter = new HeaderWriterFilter(getHeaderWriters()); + HeaderWriterFilter headersFilter = new HeaderWriterFilter( + getHeaderWriters()); headersFilter = postProcess(headersFilter); return headersFilter; } /** - * Gets the {@link HeaderWriter} instances and possibly initializes with the defaults. + * Gets the {@link HeaderWriter} instances and possibly initializes with the + * defaults. + * * @return */ private List getHeaderWriters() { - if(headerWriters.isEmpty()) { + if (headerWriters.isEmpty()) { addDefaultHeaderWriters(); } return headerWriters; diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy new file mode 100644 index 0000000000..fced68ed4f --- /dev/null +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/HeadersConfigurerTests.groovy @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2013 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.config.annotation.web.configurers + +import javax.servlet.http.HttpServletResponse + +import org.springframework.context.annotation.Configuration +import org.springframework.security.config.annotation.BaseSpringSpec +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter +import org.springframework.security.web.access.AccessDeniedHandler +import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor; +import org.springframework.security.web.util.RequestMatcher; +import org.springframework.web.servlet.support.RequestDataValueProcessor; + +import spock.lang.Unroll; + +/** + * + * @author Rob Winch + */ +class HeadersConfigurerTests extends BaseSpringSpec { + + def "headers"() { + setup: + loadConfig(HeadersConfig) + request.secure = true + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + responseHeaders == ['X-Content-Type-Options':'nosniff', + 'X-Frame-Options':'DENY', + 'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains', + 'Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate', + 'Pragma':'no-cache', + 'X-XSS-Protection' : '1; mode=block'] + } + + @Configuration + @EnableWebSecurity + static class HeadersConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.headers() + } + } + + def "headers.contentType"() { + setup: + loadConfig(ContentTypeOptionsConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + responseHeaders == ['X-Content-Type-Options':'nosniff'] + } + + @Configuration + @EnableWebSecurity + static class ContentTypeOptionsConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.headers().contentTypeOptions() + } + } + + def "headers.frameOptions"() { + setup: + loadConfig(FrameOptionsConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + responseHeaders == ['X-Frame-Options':'DENY'] + } + + @Configuration + @EnableWebSecurity + static class FrameOptionsConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.headers().frameOptions() + } + } + + def "headers.hsts"() { + setup: + loadConfig(HstsConfig) + request.secure = true + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + responseHeaders == ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains'] + } + + @Configuration + @EnableWebSecurity + static class HstsConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.headers().httpStrictTransportSecurity() + } + } + + def "headers.cacheControl"() { + setup: + loadConfig(CacheControlConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + responseHeaders == ['Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate', + 'Pragma':'no-cache'] + } + + @Configuration + @EnableWebSecurity + static class CacheControlConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.headers().cacheControl() + } + } + + def "headers.xssProtection"() { + setup: + loadConfig(XssProtectionConfig) + when: + springSecurityFilterChain.doFilter(request,response,chain) + then: + responseHeaders == ['X-XSS-Protection' : '1; mode=block'] + } + + @Configuration + @EnableWebSecurity + static class XssProtectionConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.headers().xssProtection() + } + } +} diff --git a/docs/manual/src/docbook/appendix-namespace.xml b/docs/manual/src/docbook/appendix-namespace.xml index 587db406f2..c1c9fb65a2 100644 --- a/docs/manual/src/docbook/appendix-namespace.xml +++ b/docs/manual/src/docbook/appendix-namespace.xml @@ -354,7 +354,7 @@
<literal><frame-options></literal> Attributes
- <literal>frame-options-policy</literal> + <literal>policy</literal> DENY The page cannot be displayed in a frame, regardless of @@ -372,7 +372,7 @@
- <literal>frame-options-strategy</literal> + <literal>strategy</literal> Select the AllowFromStrategy to use when using the ALLOW-FROM policy. @@ -393,18 +393,18 @@
- <literal>frame-options-ref</literal> + <literal>ref</literal> Instead of using one of the predefined strategies it is also possible to use a custom AllowFromStrategy. The reference to this bean can be specified through this ref attribute.
- <literal>frame-options-value</literal> + <literal>value</literal> The value to use when ALLOW-FROM is used a strategy.
- <literal>frame-options-from-parameter</literal> + <literal>from-parameter</literal> Specify the name of the request parameter to use when using regexp or whitelist for the ALLOW-FROM strategy. diff --git a/docs/manual/src/docbook/headers.xml b/docs/manual/src/docbook/headers.xml index 1b3ddecd8e..0e2e4222b1 100644 --- a/docs/manual/src/docbook/headers.xml +++ b/docs/manual/src/docbook/headers.xml @@ -205,7 +205,8 @@ public class WebSecurityConfig extends Another modern approach to dealing with clickjacking is using a Content Security Policy. Spring Security does not provide - support for this as the specification is not released and it is quite a bit more complicated. To stay up to date with this + support for this as the specification is not released and it is quite a bit more complicated. However, you could use the + static headers feature to implement this. To stay up to date with this issue and to see how you can implement it with Spring Security refer to SEC-2117 @@ -242,7 +243,7 @@ public class WebSecurityConfig extends } }]]>
-
+
X-XSS-Protection Some browsers have built in support for filtering out reflected @@ -276,6 +277,75 @@ public class WebSecurityConfig extends .and() ...; } +}]]> +
+
+ Static Headers + There may be times you wish to inject custom security headers into your application that are not supported out of the box. For example, perhaps + you wish to have early support for Content Security Policy in order to ensure that resources + are only loaded from the same origin. Since support for Content Security Policy has not been finalized, browsers use one of two common extension headers + to implement the feature. This means we will need to inject the policy twice. An example of the headers can be seen below: + + When using the XML namespace, these headers can be added to the response using the <header> element as + shown below: + + ... + +
+
+ +]]> + Similarly, the headers could be added to the response using Java Configuration as shown in the following: + +
+
+ Headers Writer + When the namespace or Java configuration does not support the headers you want, you can create a custom HeadersWriter instance + or even provide a custom implementation of the HeadersWriter. + Let's take a look at an example of using an custom instance of XFrameOptionsHeaderWriter. Perhaps you want to allow framing of content + for the same origin. This is easily supported by setting the policy + attribute to "SAMEORIGIN", but let's take a look at a more explicit example. + + ... + +
+ + + +]]> + We could also restrict framing of content to the same origin with Java configuration: +