2
0
Эх сурвалжийг харах

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 жил өмнө
parent
commit
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);
+    }
+}