瀏覽代碼

SEC-2230: HTTP Strict Transport Security (HSTS)Add support for Strict

This is a distinct filter as apposed to reusing StaticHeaderWriter
since the specification specifies that the "Strict-Transport-Security"
header should only be set on secure requests. It would not make sense to
require DelegatingRequestMatcherHeaderWriter since this requirement is
in the specification.
Rob Winch 12 年之前
父節點
當前提交
c85328c5d1

+ 28 - 0
config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java

@@ -28,6 +28,7 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser;
 import org.springframework.beans.factory.xml.ParserContext;
 import org.springframework.security.web.headers.Header;
 import org.springframework.security.web.headers.HeadersFilter;
+import org.springframework.security.web.headers.HstsHeaderWriter;
 import org.springframework.security.web.headers.StaticHeadersWriter;
 import org.springframework.security.web.headers.frameoptions.AbstractRequestParameterAllowFromStrategy;
 import org.springframework.security.web.headers.frameoptions.RegExpAllowFromStrategy;
@@ -57,8 +58,14 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
     private static final String ATT_VALUE = "value";
     private static final String ATT_REF = "ref";
 
+    private static final String ATT_INCLUDE_SUBDOMAINS = "include-subdomains";
+    private static final String ATT_MAX_AGE_SECONDS = "max-age-seconds";
+    private static final String ATT_REQUEST_MATCHER_REF = "request-matcher-ref";
+
     private static final String CACHE_CONTROL_ELEMENT = "cache-control";
 
+    private static final String HSTS_ELEMENT = "hsts";
+
     private static final String XSS_ELEMENT = "xss-protection";
     private static final String CONTENT_TYPE_ELEMENT = "content-type-options";
     private static final String FRAME_OPTIONS_ELEMENT = "frame-options";
@@ -76,6 +83,7 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
         BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(HeadersFilter.class);
 
         parseCacheControlElement(element);
+        parseHstsElement(element);
         parseXssElement(element, parserContext);
         parseFrameOptionsElement(element, parserContext);
         parseContentTypeOptionsElement(element);
@@ -119,6 +127,26 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
         }
     }
 
+    private void parseHstsElement(Element element) {
+        Element hstsElement = DomUtils.getChildElementByTagName(element, HSTS_ELEMENT);
+        if (hstsElement != null) {
+            BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder.genericBeanDefinition(HstsHeaderWriter.class);
+            String includeSubDomains = hstsElement.getAttribute(ATT_INCLUDE_SUBDOMAINS);
+            if(StringUtils.hasText(includeSubDomains)) {
+                headersWriter.addPropertyValue("includeSubDomains", includeSubDomains);
+            }
+            String maxAgeSeconds = hstsElement.getAttribute(ATT_MAX_AGE_SECONDS);
+            if(StringUtils.hasText(maxAgeSeconds)) {
+                headersWriter.addPropertyValue("maxAgeInSeconds", maxAgeSeconds);
+            }
+            String requestMatcherRef = hstsElement.getAttribute(ATT_REQUEST_MATCHER_REF);
+            if(StringUtils.hasText(requestMatcherRef)) {
+                headersWriter.addPropertyReference("requestMatcher", requestMatcherRef);
+            }
+            headerWriters.add(headersWriter.getBeanDefinition());
+        }
+    }
+
     private void parseHeaderElements(Element element) {
         List<Element> headerElts = DomUtils.getChildElementsByTagName(element, GENERIC_HEADER_ELEMENT);
         for (Element headerElt : headerElts) {

+ 14 - 1
config/src/main/resources/org/springframework/security/config/spring-security-3.2.rnc

@@ -720,7 +720,20 @@ jdbc-user-service.attlist &=
 
 headers =
  ## Element for configuration of the AddHeadersFilter. Enables easy setting for the X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers.
- element headers {cache-control? & xss-protection? & frame-options? & content-type-options? & header*}
+ element headers {cache-control? & xss-protection? & hsts? & frame-options? & content-type-options? & header*}
+
+hsts =
+    ## Adds support for HTTP Strict Transport Security (HSTS)
+    element hsts {hsts-options.attlist}
+hsts-options.attlist &=
+    ## Specifies if subdomains should be included. Default true.
+    attribute include-subdomains {xsd:boolean}?
+hsts-options.attlist &=
+    ## Specifies the maximum ammount of time the host should be considered a Known HSTS Host. Default one year.
+    attribute max-age-seconds {xsd:integer}?
+hsts-options.attlist &=
+    ## The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true.
+    attribute request-matcher-ref { xsd:token }?
 
 cache-control =
     ## Adds Cache-Control no-cache, no-store, must-revalidate and Pragma no-cache every URL

+ 32 - 0
config/src/main/resources/org/springframework/security/config/spring-security-3.2.xsd

@@ -2242,12 +2242,44 @@
          <xs:choice minOccurs="0" maxOccurs="unbounded">
             <xs:element ref="security:cache-control"/>
             <xs:element ref="security:xss-protection"/>
+            <xs:element ref="security:hsts"/>
             <xs:element ref="security:frame-options"/>
             <xs:element ref="security:content-type-options"/>
             <xs:element ref="security:header"/>
          </xs:choice>
       </xs:complexType>
    </xs:element>
+  <xs:element name="hsts">
+      <xs:annotation>
+         <xs:documentation>Adds support for HTTP Strict Transport Security (HSTS)
+                </xs:documentation>
+      </xs:annotation>
+      <xs:complexType>
+         <xs:attributeGroup ref="security:hsts-options.attlist"/>
+      </xs:complexType>
+   </xs:element>
+  <xs:attributeGroup name="hsts-options.attlist">
+      <xs:attribute name="include-subdomains" type="xs:boolean">
+         <xs:annotation>
+            <xs:documentation>Specifies if subdomains should be included. Default true.
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="max-age-seconds" type="xs:integer">
+         <xs:annotation>
+            <xs:documentation>Specifies the maximum ammount of time the host should be considered a Known HSTS Host.
+                Default one year.
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+      <xs:attribute name="request-matcher-ref" type="xs:token">
+         <xs:annotation>
+            <xs:documentation>The RequestMatcher instance to be used to determine if the header should be set. Default
+                is if HttpServletRequest.isSecure() is true.
+                </xs:documentation>
+         </xs:annotation>
+      </xs:attribute>
+  </xs:attributeGroup>
   <xs:element name="cache-control">
       <xs:annotation>
          <xs:documentation>Adds Cache-Control no-cache, no-store, must-revalidate and Pragma no-cache every URL

+ 51 - 0
config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy

@@ -35,6 +35,7 @@ import org.springframework.security.web.authentication.ui.DefaultLoginPageGenera
 import org.springframework.security.web.headers.HeadersFilter
 import org.springframework.security.web.headers.StaticHeadersWriter;
 import org.springframework.security.web.headers.frameoptions.StaticAllowFromStrategy;
+import org.springframework.security.web.util.AnyRequestMatcher;
 
 /**
  *
@@ -341,6 +342,56 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
             assertHeaders(response, ['Cache-Control': 'no-cache,no-store,max-age=0,must-revalidate','Pragma':'no-cache'])
     }
 
+    def 'http headers hsts'() {
+        setup:
+            httpAutoConfig {
+                'headers'() {
+                    'hsts'()
+                }
+            }
+            createAppContext()
+            def springSecurityFilterChain = appContext.getBean(FilterChainProxy)
+            MockHttpServletResponse response = new MockHttpServletResponse()
+        when:
+            springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
+        then:
+            assertHeaders(response, ['Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains'])
+    }
+
+    def 'http headers hsts default only invokes on HttpServletRequest.isSecure = true'() {
+        setup:
+            httpAutoConfig {
+                'headers'() {
+                    'hsts'()
+                }
+            }
+            createAppContext()
+            def springSecurityFilterChain = appContext.getBean(FilterChainProxy)
+            MockHttpServletResponse response = new MockHttpServletResponse()
+        when:
+            springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
+        then:
+            response.headerNames.empty
+    }
+
+    def 'http headers hsts custom'() {
+        setup:
+            httpAutoConfig {
+                'headers'() {
+                    'hsts'('max-age-seconds':'1','include-subdomains':false, 'request-matcher-ref' : 'matcher')
+                }
+            }
+
+            xml.'b:bean'(id: 'matcher', 'class': AnyRequestMatcher.name)
+            createAppContext()
+            def springSecurityFilterChain = appContext.getBean(FilterChainProxy)
+            MockHttpServletResponse response = new MockHttpServletResponse()
+        when:
+            springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
+        then:
+            assertHeaders(response, ['Strict-Transport-Security': 'max-age=1'])
+    }
+
     def assertHeaders(MockHttpServletResponse response, Map<String,String> expected) {
         assert response.headerNames == expected.keySet()
         expected.each { headerName, value ->

+ 36 - 0
docs/manual/src/docbook/appendix-namespace.xml

@@ -267,6 +267,9 @@
                     <listitem><literal>Cache-Control</literal> and <literal>Pragma</literal> - Can be set using the
                         <link xlink:href="#nsa-cache-control">cache-control</link> element. This ensures that the
                         browser does not cache your secured pages.</listitem>
+                    <listitem><literal>Strict-Transport-Security</literal> - Can be set using the
+                        <link xlink:href="#nsa-hsts">hsts</link> element. This ensures that the
+                        browser automatically requests HTTPS for future requests.</listitem>
                     <listitem><literal>X-Frame-Options</literal> - Can be set using the
                         <link xlink:href="#nsa-frame-options">frame-options</link> element. The
                         <link xlink:href="http://en.wikipedia.org/wiki/Clickjacking#X-Frame-Options">X-Frame-Options
@@ -295,6 +298,7 @@
                     <listitem><link xlink:href="#nsa-content-type-options">content-type-options</link></listitem>
                     <listitem><link xlink:href="#nsa-frame-options">frame-options</link></listitem>
                     <listitem><link xlink:href="#nsa-header">header</link></listitem>
+                    <listitem><link xlink:href="#nsa-hsts">hsts</link></listitem>
                     <listitem><link xlink:href="#nsa-xss-protection">xss-protection</link></listitem>
                 </itemizedlist>
             </section>
@@ -310,6 +314,38 @@
                 </itemizedlist>
             </section>
         </section>
+        <section xml:id="nsa-hsts">
+            <title><literal>&lt;hsts&gt;</literal></title>
+            <para>When enabled adds the <link xlink:href="http://tools.ietf.org/html/rfc6797">Strict-Transport-Security</link> header to the response
+                for any secure request. This allows the server to instruct browsers to automatically use HTTPS for future requests.</para>
+            <section xml:id="nsa-hsts-attributes">
+                <title><literal>&lt;hsts&gt;</literal> Attributes</title>
+                <section xml:id="nsa-hsts-include-subdomains">
+                    <title><literal>include-sub-domains</literal></title>
+                    <para>
+                        Specifies if subdomains should be included. Default true.
+                    </para>
+                </section>
+                <section xml:id="nsa-hsts-max-age-seconds">
+                    <title><literal>max-age-seconds</literal></title>
+                    <para>
+                        Specifies the maximum ammount of time the host should be considered a Known HSTS Host. Default one year.
+                    </para>
+                </section>
+                <section xml:id="nsa-hsts-request-matcher-ref">
+                    <title><literal>request-matcher-ref</literal></title>
+                    <para>
+                        The RequestMatcher instance to be used to determine if the header should be set. Default is if HttpServletRequest.isSecure() is true.
+                    </para>
+                </section>
+            </section>
+             <section xml:id="nsa-hsts-parents">
+                <title>Parent Elements of <literal>&lt;hsts&gt;</literal></title>
+                <itemizedlist>
+                    <listitem><link xlink:href="#nsa-headers">headers</link></listitem>
+                </itemizedlist>
+            </section>
+        </section>
         <section xml:id="nsa-frame-options">
             <title><literal>&lt;frame-options&gt;</literal></title>
             <para>When enabled adds the <link xlink:href="http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-01">X-Frame-Options header</link> to the response, this allows newer browsers to do some security

+ 165 - 0
web/src/main/java/org/springframework/security/web/headers/HstsHeaderWriter.java

@@ -0,0 +1,165 @@
+/*
+ * 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.web.headers;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.security.web.util.RequestMatcher;
+import org.springframework.util.Assert;
+
+/**
+ * Provides support for <a href="http://tools.ietf.org/html/rfc6797">HTTP Strict
+ * Transport Security (HSTS)</a>.
+ *
+ * <p>
+ * By default the expiration is one year and subdomains will be included. This
+ * can be customized using {@link #setMaxAgeInSeconds(long)} and
+ * {@link #setIncludeSubDomains(boolean)} respectively.
+ * </p>
+ *
+ * <p>
+ * Since <a href="http://tools.ietf.org/html/rfc6797#section-7.2">section
+ * 7.2</a> states that HSTS Host MUST NOT include the STS header in HTTP
+ * responses, the default behavior is that the "Strict-Transport-Security" will
+ * only be added when {@link HttpServletRequest#isSecure()} returns {@code true}
+ * . At times this may need to be customized. For example, in some situations
+ * where SSL termination is used, something else may be used to determine if SSL
+ * was used. For these circumstances, {@link #setRequestMatcher(RequestMatcher)}
+ * can be invoked with a custom {@link RequestMatcher}.
+ * </p>
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class HstsHeaderWriter implements HeaderWriter {
+    private static final String HSTS_HEADER_NAME = "Strict-Transport-Security";
+
+    private final Log logger = LogFactory.getLog(getClass());
+
+    private RequestMatcher requestMatcher = new SecureRequestMatcher();
+
+    private long maxAgeInSeconds;
+
+    private boolean includeSubDomains;
+
+    private String hstsHeaderValue;
+
+    public HstsHeaderWriter() {
+        this.maxAgeInSeconds = 31536000;
+        this.includeSubDomains = true;
+        updateHstsHeaderValue();
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see
+     * org.springframework.security.web.headers.HeaderWriter#writeHeaders(javax
+     * .servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
+     */
+    @Override
+    public void writeHeaders(HttpServletRequest request,
+            HttpServletResponse response) {
+        if (requestMatcher.matches(request)) {
+            response.setHeader(HSTS_HEADER_NAME, hstsHeaderValue);
+        } else if (logger.isDebugEnabled()) {
+            logger.debug("Not injecting HSTS header since it did not match the requestMatcher "
+                    + requestMatcher);
+        }
+    }
+
+    /**
+     * Sets the {@link RequestMatcher} used to determine if the
+     * "Strict-Transport-Security" should be added. If true the header is added,
+     * else the header is not added. By default the header is added when
+     * {@link HttpServletRequest#isSecure()} returns true.
+     *
+     * @param requestMatcher
+     *            the {@link RequestMatcher} to use.
+     * @throws IllegalArgumentException
+     *             if {@link RequestMatcher} is null
+     */
+    public void setRequestMatcher(RequestMatcher requestMatcher) {
+        Assert.notNull(requestMatcher, "requestMatcher cannot be null");
+        this.requestMatcher = requestMatcher;
+    }
+
+    /**
+     * <p>
+     * Sets the value (in seconds) for the max-age directive of the
+     * Strict-Transport-Security header. The default is one year.
+     * </p>
+     *
+     * <p>
+     * This instructs browsers how long to remember to keep this domain as a
+     * known HSTS Host. See <a
+     * href="http://tools.ietf.org/html/rfc6797#section-6.1.1">Section 6.1.1</a>
+     * for additional details.
+     * </p>
+     *
+     * @param maxAgeInSeconds
+     *            the maximum amount of time (in seconds) to consider this
+     *            domain as a known HSTS Host.
+     * @throws IllegalArgumentException
+     *             if maxAgeInSeconds is negative
+     */
+    public void setMaxAgeInSeconds(long maxAgeInSeconds) {
+        if (maxAgeInSeconds < 0) {
+            throw new IllegalArgumentException(
+                    "maxAgeInSeconds must be non-negative. Got "
+                            + maxAgeInSeconds);
+        }
+        this.maxAgeInSeconds = maxAgeInSeconds;
+        updateHstsHeaderValue();
+    }
+
+    /**
+     * <p>
+     * If true, subdomains should be considered HSTS Hosts too. The default is
+     * true.
+     * </p>
+     *
+     * <p>
+     * See <a href="http://tools.ietf.org/html/rfc6797#section-6.1.2">Section
+     * 6.1.2</a> for additional details.
+     * </p>
+     *
+     * @param includeSubDomains
+     *            true to include subdomains, else false
+     */
+    public void setIncludeSubDomains(boolean includeSubDomains) {
+        this.includeSubDomains = includeSubDomains;
+        updateHstsHeaderValue();
+    }
+
+    private void updateHstsHeaderValue() {
+        String headerValue = "max-age=" + maxAgeInSeconds;
+        if (includeSubDomains) {
+            headerValue += " ; includeSubDomains";
+        }
+        this.hstsHeaderValue = headerValue;
+    }
+
+    private static final class SecureRequestMatcher implements RequestMatcher {
+        @Override
+        public boolean matches(HttpServletRequest request) {
+            return request.isSecure();
+        }
+    }
+}

+ 90 - 0
web/src/test/java/org/springframework/security/web/headers/HstsHeaderWriterTests.java

@@ -0,0 +1,90 @@
+/*
+ * 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.web.headers;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+/**
+ * @author Rob Winch
+ *
+ */
+public class HstsHeaderWriterTests {
+    private MockHttpServletRequest request;
+    private MockHttpServletResponse response;
+
+    private HstsHeaderWriter writer;
+
+    @Before
+    public void setup() {
+        request = new MockHttpServletRequest();
+        request.setSecure(true);
+        response = new MockHttpServletResponse();
+
+        writer = new HstsHeaderWriter();
+    }
+
+    @Test
+    public void writeHeadersDefaultValues() {
+        writer.writeHeaders(request, response);
+
+        assertThat(response.getHeaderNames().size()).isEqualTo(1);
+        assertThat(response.getHeader("Strict-Transport-Security")).isEqualTo("max-age=31536000 ; includeSubDomains");
+    }
+
+    @Test
+    public void writeHeadersIncludeSubDomainsFalse() {
+        writer.setIncludeSubDomains(false);
+
+        writer.writeHeaders(request, response);
+
+        assertThat(response.getHeaderNames().size()).isEqualTo(1);
+        assertThat(response.getHeader("Strict-Transport-Security")).isEqualTo("max-age=31536000");
+    }
+
+    @Test
+    public void writeHeadersCustomMaxAgeInSeconds() {
+        writer.setMaxAgeInSeconds(1);
+
+        writer.writeHeaders(request, response);
+
+        assertThat(response.getHeaderNames().size()).isEqualTo(1);
+        assertThat(response.getHeader("Strict-Transport-Security")).isEqualTo("max-age=1 ; includeSubDomains");
+    }
+
+    @Test
+    public void writeHeadersInsecureRequestDoesNotWriteHeader() {
+        request.setSecure(false);
+
+        writer.writeHeaders(request, response);
+
+        assertThat(response.getHeaderNames().isEmpty()).isTrue();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setMaxAgeInSecondsToNegative() {
+        writer.setMaxAgeInSeconds(-1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void setRequestMatcherToNull() {
+        writer.setRequestMatcher(null);
+    }
+}