SEC-2846: Security HTTP Response Headers Configuration Cleanup

This commit is contained in:
Rob Winch
2015-02-06 12:59:05 -06:00
parent 9c5cb2f438
commit c4fe630f8e
8 changed files with 1042 additions and 245 deletions

View File

@@ -26,6 +26,7 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
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.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode;
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
@@ -77,7 +78,10 @@ class HeadersConfigurerTests extends BaseSpringSpec {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().contentTypeOptions()
http
.headers()
.defaultsDisabled()
.contentTypeOptions()
}
}
@@ -95,7 +99,10 @@ class HeadersConfigurerTests extends BaseSpringSpec {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions()
http
.headers()
.defaultsDisabled()
.frameOptions()
}
}
@@ -114,7 +121,10 @@ class HeadersConfigurerTests extends BaseSpringSpec {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().httpStrictTransportSecurity()
http
.headers()
.defaultsDisabled()
.httpStrictTransportSecurity()
}
}
@@ -134,7 +144,10 @@ class HeadersConfigurerTests extends BaseSpringSpec {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().cacheControl()
http
.headers()
.defaultsDisabled()
.cacheControl()
}
}
@@ -152,7 +165,37 @@ class HeadersConfigurerTests extends BaseSpringSpec {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().xssProtection()
http
.headers()
.defaultsDisabled()
.xssProtection()
}
}
def "headers custom x-frame-options"() {
setup:
loadConfig(HeadersCustomSameOriginConfig)
request.secure = true
when:
springSecurityFilterChain.doFilter(request,response,chain)
then:
responseHeaders == ['X-Content-Type-Options':'nosniff',
'X-Frame-Options':'SAMEORIGIN',
'Strict-Transport-Security': 'max-age=31536000 ; includeSubDomains',
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate',
'Expires' : '0',
'Pragma':'no-cache',
'X-XSS-Protection' : '1; mode=block']
}
@EnableWebSecurity
static class HeadersCustomSameOriginConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers()
.frameOptions().sameOrigin()
}
}
}

View File

@@ -28,6 +28,7 @@ import org.springframework.security.web.header.writers.frameoptions.StaticAllowF
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter.XFrameOptionsMode
import org.springframework.security.web.util.matcher.AnyRequestMatcher
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
* Tests to verify that all the functionality of <headers> attributes is present
@@ -80,7 +81,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
protected void configure(HttpSecurity http) {
http
.headers()
.addHeaderWriter(new CacheControlHeadersWriter())
.defaultsDisabled()
.cacheControl()
}
}
@@ -100,7 +102,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
protected void configure(HttpSecurity http) {
http
.headers()
.addHeaderWriter(new HstsHeaderWriter())
.defaultsDisabled()
.httpStrictTransportSecurity()
}
}
@@ -120,8 +123,11 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
http
.headers()
// hsts@request-matcher-ref, hsts@max-age-seconds, hsts@include-subdomains
// Additional Constructors are provided to leverage default values
.addHeaderWriter(new HstsHeaderWriter(AnyRequestMatcher.INSTANCE, 15768000, false))
.defaultsDisabled()
.httpStrictTransportSecurity()
.requestMatcher(AnyRequestMatcher.INSTANCE)
.maxAgeInSeconds(15768000)
.includeSubDomains(false)
}
}
@@ -141,7 +147,9 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
http
.headers()
// frame-options@policy=SAMEORIGIN
.addHeaderWriter(new XFrameOptionsHeaderWriter(XFrameOptionsMode.SAMEORIGIN))
.defaultsDisabled()
.frameOptions()
.sameOrigin()
}
}
@@ -164,6 +172,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
http
.headers()
// frame-options@ref
.defaultsDisabled()
.addHeaderWriter(new XFrameOptionsHeaderWriter(new StaticAllowFromStrategy(new URI("https://example.com"))))
}
}
@@ -184,7 +193,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
http
.headers()
// xss-protection
.addHeaderWriter(new XXssProtectionHeaderWriter())
.defaultsDisabled()
.xssProtection()
}
}
@@ -204,7 +214,10 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
http
.headers()
// xss-protection@enabled and xss-protection@block
.addHeaderWriter(new XXssProtectionHeaderWriter(enabled:true,block:false))
.defaultsDisabled()
.xssProtection()
.xssProtectionEnabled(true)
.block(false)
}
}
@@ -224,7 +237,8 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
http
.headers()
// content-type-options
.addHeaderWriter(new XContentTypeOptionsHeaderWriter())
.defaultsDisabled()
.contentTypeOptions()
}
}
@@ -245,6 +259,7 @@ public class NamespaceHttpHeadersTests extends BaseSpringSpec {
protected void configure(HttpSecurity http) {
http
.headers()
.defaultsDisabled()
.addHeaderWriter(new StaticHeadersWriter("customHeaderName", "customHeaderValue"))
}
}

View File

@@ -27,6 +27,13 @@ import org.springframework.security.web.util.matcher.AnyRequestMatcher
* @author Rob Winch
*/
class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def defaultHeaders = ['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',
'Expires' : '0',
'Pragma':'no-cache',
'X-XSS-Protection' : '1; mode=block']
def 'headers disabled'() {
setup:
httpAutoConfig {
@@ -62,13 +69,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
then:
assertHeaders(response, ['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',
'Expires' : '0',
'Pragma':'no-cache',
'X-XSS-Protection' : '1; mode=block'])
assertHeaders(response, defaultHeaders)
}
def 'http headers with empty headers'() {
@@ -82,18 +83,33 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
then:
assertHeaders(response, ['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',
'Expires' : '0',
'Pragma':'no-cache',
'X-XSS-Protection' : '1; mode=block'])
assertHeaders(response, defaultHeaders)
}
def 'http headers frame-options@policy=SAMEORIGIN with defaults'() {
httpAutoConfig {
'headers'() {
'frame-options'(policy:'SAMEORIGIN')
}
}
createAppContext()
def hf = getFilter(HeaderWriterFilter)
MockHttpServletResponse response = new MockHttpServletResponse()
hf.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
def expectedHeaders = [:] << defaultHeaders
expectedHeaders['X-Frame-Options'] = 'SAMEORIGIN'
expect:
assertHeaders(response, expectedHeaders)
}
// --- defaults disabled
def 'http headers content-type-options'() {
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'content-type-options'()
}
}
@@ -109,7 +125,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers frame-options defaults to DENY'() {
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'frame-options'()
}
}
@@ -125,7 +141,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers frame-options DENY'() {
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'frame-options'(policy : 'DENY')
}
}
@@ -141,7 +157,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers frame-options SAMEORIGIN'() {
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'frame-options'(policy : 'SAMEORIGIN')
}
}
@@ -158,7 +174,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers frame-options ALLOW-FROM no origin reports error'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'frame-options'(policy : 'ALLOW-FROM', strategy : 'static')
}
}
@@ -174,7 +190,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers frame-options ALLOW-FROM spaces only origin reports error'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'frame-options'(policy : 'ALLOW-FROM', strategy: 'static', value : ' ')
}
}
@@ -190,7 +206,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers frame-options ALLOW-FROM'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'frame-options'(policy : 'ALLOW-FROM', strategy: 'static', value : 'https://example.com')
}
}
@@ -207,7 +223,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers frame-options ALLOW-FROM with whitelist strategy'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'frame-options'(policy : 'ALLOW-FROM', strategy: 'whitelist', value : 'https://example.com')
}
}
@@ -227,7 +243,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers header a=b'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'header'(name : 'a', value: 'b')
}
}
@@ -244,7 +260,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers header a=b and c=d'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'header'(name : 'a', value: 'b')
'header'(name : 'c', value: 'd')
}
@@ -262,7 +278,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers with ref'() {
setup:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'header'(ref:'headerWriter')
}
}
@@ -282,7 +298,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers header no name produces error'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'header'(value: 'b')
}
}
@@ -295,7 +311,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers header no value produces error'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'header'(name: 'a')
}
}
@@ -308,7 +324,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers xss-protection defaults'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'xss-protection'()
}
}
@@ -325,7 +341,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers xss-protection enabled=true'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'xss-protection'(enabled:'true')
}
}
@@ -342,7 +358,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers xss-protection enabled=false'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'xss-protection'(enabled:'false')
}
}
@@ -359,7 +375,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers xss-protection enabled=false and block=true produces error'() {
when:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'xss-protection'(enabled:'false', block:'true')
}
}
@@ -375,7 +391,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers cache-control'() {
setup:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'cache-control'()
}
}
@@ -393,7 +409,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers hsts'() {
setup:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'hsts'()
}
}
@@ -409,7 +425,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers hsts default only invokes on HttpServletRequest.isSecure = true'() {
setup:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'hsts'()
}
}
@@ -425,7 +441,7 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
def 'http headers hsts custom'() {
setup:
httpAutoConfig {
'headers'() {
'headers'('defaults-disabled':true) {
'hsts'('max-age-seconds':'1','include-subdomains':false, 'request-matcher-ref' : 'matcher')
}
}
@@ -440,6 +456,187 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
assertHeaders(response, ['Strict-Transport-Security': 'max-age=1'])
}
// --- disable single default header ---
def 'http headers cache-controls@disabled=true'() {
setup:
httpAutoConfig {
'headers'() {
'cache-control'(disabled:true)
}
}
createAppContext()
def springSecurityFilterChain = appContext.getBean(FilterChainProxy)
MockHttpServletResponse response = new MockHttpServletResponse()
def expectedHeaders = [:] << defaultHeaders
expectedHeaders.remove('Cache-Control')
expectedHeaders.remove('Expires')
expectedHeaders.remove('Pragma')
when:
springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
then:
assertHeaders(response, expectedHeaders)
}
def 'http headers content-type-options@disabled=true'() {
setup:
httpAutoConfig {
'headers'() {
'content-type-options'(disabled:true)
}
}
createAppContext()
def springSecurityFilterChain = appContext.getBean(FilterChainProxy)
MockHttpServletResponse response = new MockHttpServletResponse()
def expectedHeaders = [:] << defaultHeaders
expectedHeaders.remove('X-Content-Type-Options')
when:
springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
then:
assertHeaders(response, expectedHeaders)
}
def 'http headers hsts@disabled=true'() {
setup:
httpAutoConfig {
'headers'() {
'hsts'(disabled:true)
}
}
createAppContext()
def springSecurityFilterChain = appContext.getBean(FilterChainProxy)
MockHttpServletResponse response = new MockHttpServletResponse()
def expectedHeaders = [:] << defaultHeaders
expectedHeaders.remove('Strict-Transport-Security')
when:
springSecurityFilterChain.doFilter(new MockHttpServletRequest(), response, new MockFilterChain())
then:
assertHeaders(response, expectedHeaders)
}
def 'http headers frame-options@disabled=true'() {
setup:
httpAutoConfig {
'headers'() {
'frame-options'(disabled:true)
}
}
createAppContext()
def springSecurityFilterChain = appContext.getBean(FilterChainProxy)
MockHttpServletResponse response = new MockHttpServletResponse()
def expectedHeaders = [:] << defaultHeaders
expectedHeaders.remove('X-Frame-Options')
when:
springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
then:
assertHeaders(response, expectedHeaders)
}
def 'http headers xss-protection@disabled=true'() {
setup:
httpAutoConfig {
'headers'() {
'xss-protection'(disabled:true)
}
}
createAppContext()
def springSecurityFilterChain = appContext.getBean(FilterChainProxy)
MockHttpServletResponse response = new MockHttpServletResponse()
def expectedHeaders = [:] << defaultHeaders
expectedHeaders.remove('X-XSS-Protection')
when:
springSecurityFilterChain.doFilter(new MockHttpServletRequest(secure:true), response, new MockFilterChain())
then:
assertHeaders(response, expectedHeaders)
}
// --- disable error handling ---
def 'http headers hsts@disabled=true no include-subdomains'() {
setup:
httpAutoConfig {
'headers'() {
'hsts'(disabled:true,'include-subdomains':true)
}
}
when:
createAppContext()
then:
BeanDefinitionParsingException expected = thrown()
expected.message.contains 'include-subdomains'
}
def 'http headers hsts@disabled=true no max-age'() {
setup:
httpAutoConfig {
'headers'() {
'hsts'(disabled:true,'max-age-seconds':123)
}
}
when:
createAppContext()
then:
BeanDefinitionParsingException expected = thrown()
expected.message.contains 'max-age'
}
def 'http headers hsts@disabled=true no matcher-ref'() {
setup:
httpAutoConfig {
'headers'() {
'hsts'(disabled:true,'request-matcher-ref':'matcher')
}
}
xml.'b:bean'(id: 'matcher', 'class': AnyRequestMatcher.name)
when:
createAppContext()
then:
BeanDefinitionParsingException expected = thrown()
expected.message.contains 'request-matcher-ref'
}
def 'http xss@disabled=true no enabled'() {
setup:
httpAutoConfig {
'headers'() {
'xss-protection'(disabled:true,'enabled':true)
}
}
when:
createAppContext()
then:
BeanDefinitionParsingException expected = thrown()
expected.message.contains 'enabled'
}
def 'http xss@disabled=true no block'() {
setup:
httpAutoConfig {
'headers'() {
'xss-protection'(disabled:true,'block':true)
}
}
when:
createAppContext()
then:
BeanDefinitionParsingException expected = thrown()
expected.message.contains 'block'
}
def 'http frame-options@disabled=true no policy'() {
setup:
httpAutoConfig {
'headers'() {
'frame-options'(disabled:true,'policy':'DENY')
}
}
when:
createAppContext()
then:
BeanDefinitionParsingException expected = thrown()
expected.message.contains 'policy'
}
def assertHeaders(MockHttpServletResponse response, Map<String,String> expected) {
assert response.headerNames == expected.keySet()
expected.each { headerName, value ->