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.
This commit is contained in:
Rob Winch
2013-07-29 18:22:28 -05:00
parent 8013cd54d6
commit c85328c5d1
7 changed files with 416 additions and 1 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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

View File

@@ -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 ->