1
0
mirror of synced 2026-08-04 17:27:13 +00:00

SEC-2098, SEC-2099: Created HeadersFilter

Created HeadersFilter for setting security headers added including a
bean definition parser for easy configuration of the headers. Enables
easy configuration for the X-Frame-Options, X-XSS-Protection and
X-Content-Type-Options headers. Also allows for additional headers to
be added.
This commit is contained in:
Marten Deinum
2012-12-21 14:56:18 +01:00
committed by Rob Winch
parent f5a30e55a3
commit 0adf5aea91
13 changed files with 573 additions and 5 deletions
@@ -54,4 +54,5 @@ public abstract class Elements {
public static final String LDAP_PASSWORD_COMPARE = "password-compare";
public static final String DEBUG = "debug";
public static final String HTTP_FIREWALL = "http-firewall";
public static final String ADD_HEADERS = "add-headers";
}
@@ -0,0 +1,124 @@
/*
* Copyright 2002-2012 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.http;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.web.headers.HeadersFilter;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Parser for the {@code HeadersFilter}.
*
* @author Marten Deinum
* @since 3.2
*/
public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
private static final String ATT_ENABLED = "enabled";
private static final String ATT_BLOCK = "block";
private static final String ATT_POLICY = "policy";
private static final String ATT_ORIGIN = "origin";
private static final String ATT_NAME = "name";
private static final String ATT_VALUE = "value";
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";
private static final String GENERIC_HEADER_ELEMENT = "header";
private static final String XSS_PROTECTION_HEADER = "X-XSS-Protection";
private static final String FRAME_OPTIONS_HEADER = "X-Frame-Options";
private static final String CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options";
private static final String ALLOW_FROM = "ALLOW-FROM";
public BeanDefinition parse(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(HeadersFilter.class);
final Map<String, String> headers = new HashMap<String, String>();
parseXssElement(element, headers);
parseFrameOptionsElement(element, parserContext, headers);
parseContentTypeOptionsElement(element, headers);
parseHeaderElements(element, headers);
builder.addPropertyValue("headers", headers);
return builder.getBeanDefinition();
}
private void parseHeaderElements(Element element, Map<String, String> headers) {
List<Element> headerEtls = DomUtils.getChildElementsByTagName(element, GENERIC_HEADER_ELEMENT);
for (Element headerEtl : headerEtls) {
headers.put(headerEtl.getAttribute(ATT_NAME), headerEtl.getAttribute(ATT_VALUE));
}
}
private void parseContentTypeOptionsElement(Element element, Map<String, String> headers) {
Element contentTypeElt = DomUtils.getChildElementByTagName(element, CONTENT_TYPE_ELEMENT);
if (contentTypeElt != null) {
headers.put(CONTENT_TYPE_OPTIONS_HEADER, "nosniff");
}
}
private void parseFrameOptionsElement(Element element, ParserContext parserContext, Map<String, String> headers) {
Element frameElt = DomUtils.getChildElementByTagName(element, FRAME_OPTIONS_ELEMENT);
if (frameElt != null) {
String header = getAttribute(frameElt, ATT_POLICY, "DENY");
if (ALLOW_FROM.equals(header) ) {
String origin = frameElt.getAttribute(ATT_ORIGIN);
if (!StringUtils.hasText(origin) ) {
parserContext.getReaderContext().error("Frame options header value ALLOW-FROM required an origin to be specified.", frameElt);
}
header += " " + origin;
}
headers.put(FRAME_OPTIONS_HEADER, header);
}
}
private void parseXssElement(Element element, Map<String, String> headers) {
Element xssElt = DomUtils.getChildElementByTagName(element, XSS_ELEMENT);
if (xssElt != null) {
boolean enabled = Boolean.valueOf(getAttribute(xssElt, ATT_ENABLED, "true"));
boolean block = Boolean.valueOf(getAttribute(xssElt, ATT_BLOCK, "true"));
String value = enabled ? "1" : "0";
if (enabled && block) {
value += "; mode=block";
}
headers.put(XSS_PROTECTION_HEADER, value);
}
}
private String getAttribute(Element element, String name, String defaultValue) {
String value = element.getAttribute(name);
if (StringUtils.hasText(value)) {
return value;
} else {
return defaultValue;
}
}
}
@@ -117,6 +117,7 @@ class HttpConfigurationBuilder {
private final BeanReference portResolver;
private BeanReference fsi;
private BeanReference requestCache;
private BeanDefinition addHeadersFilter;
public HttpConfigurationBuilder(Element element, ParserContext pc,
BeanReference portMapper, BeanReference portResolver, BeanReference authenticationManager) {
@@ -151,6 +152,7 @@ class HttpConfigurationBuilder {
createJaasApiFilter();
createChannelProcessingFilter();
createFilterSecurityInterceptor(authenticationManager);
createAddHeadersFilter();
}
@SuppressWarnings("rawtypes")
@@ -554,6 +556,14 @@ class HttpConfigurationBuilder {
this.fsi = new RuntimeBeanReference(fsiId);
}
private void createAddHeadersFilter() {
Element elmt = DomUtils.getChildElementByTagName(httpElt, Elements.ADD_HEADERS);
if (elmt != null) {
this.addHeadersFilter = new HeadersBeanDefinitionParser().parse(elmt, pc);
}
}
BeanReference getSessionStrategy() {
return sessionStrategyRef;
}
@@ -601,6 +611,10 @@ class HttpConfigurationBuilder {
filters.add(new OrderDecorator(requestCacheAwareFilter, REQUEST_CACHE_FILTER));
}
if (addHeadersFilter != null) {
filters.add(new OrderDecorator(addHeadersFilter, HEADERS_FILTER));
}
return filters;
}
}
@@ -29,6 +29,7 @@ enum SecurityFilters {
CONCURRENT_SESSION_FILTER,
/** {@link WebAsyncManagerIntegrationFilter} */
WEB_ASYNC_MANAGER_FILTER,
HEADERS_FILTER,
LOGOUT_FILTER,
X509_FILTER,
PRE_AUTH_FILTER,
@@ -281,7 +281,7 @@ http-firewall =
http =
## Container element for HTTP security configuration. Multiple elements can now be defined, each with a specific pattern to which the enclosed security configuration applies. A pattern can also be configured to bypass Spring Security's filters completely by setting the "secured" attribute to "false".
element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler?) }
element http {http.attlist, (intercept-url* & access-denied-handler? & form-login? & openid-login? & x509? & jee? & http-basic? & logout? & session-management & remember-me? & anonymous? & port-mappings & custom-filter* & request-cache? & expression-handler? & headers?) }
http.attlist &=
## The request URL pattern which will be mapped to the filter chain created by this <http> element. If omitted, the filter chain will match all requests.
attribute pattern {xsd:token}?
@@ -718,6 +718,43 @@ jdbc-user-service.attlist &=
jdbc-user-service.attlist &=
role-prefix?
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 {xss-protection? & frame-options? & content-type-options? & header*}
frame-options =
## Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options header.
element frame-options {frame-options.attlist,empty}
frame-options.attlist &=
## Specify the policy to use for the X-Frame-Options-Header.
attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}?
frame-options.attlist &=
## Specify the origin to use when ALLOW-FROM is chosen.
attribute origin {xsd:token}?
xss-protection =
## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header.
element xss-protection {xss-protection.attlist,empty}
xss-protection.attlist &=
## enable or disable the X-XSS-Protection header. Default is 'true' meaning it is enabled.
attribute enabled {xsd:boolean}?
xss-protection.attlist &=
## Add mode=block to the header or not, default is on.
attribute block {xsd:boolean}?
content-type-options =
## Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'.
element content-type-options {empty}
header=
## Add additional headers to the response.
element header {header.attlist}
header.attlist &=
## The name of the header to add.
attribute name {xsd:token}
header.attlist &=
## The value for the header.
attribute value {xsd:token}
any-user-service = user-service | jdbc-user-service | ldap-user-service
@@ -1024,6 +1024,7 @@
<xs:attributeGroup ref="security:ref"/>
</xs:complexType>
</xs:element>
<xs:element ref="security:headers"/>
</xs:choice>
<xs:attributeGroup ref="security:http.attlist"/>
</xs:complexType>
@@ -2231,6 +2232,106 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="headers">
<xs:annotation>
<xs:documentation>Element for configuration of the AddHeadersFilter. Enables easy setting for the
X-Frame-Options, X-XSS-Protection and X-Content-Type-Options headers.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="security:xss-protection"/>
<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="frame-options">
<xs:annotation>
<xs:documentation>Enable basic clickjacking support for newer browsers (IE8+), will set the X-Frame-Options
header.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attributeGroup ref="security:frame-options.attlist"/>
</xs:complexType>
</xs:element>
<xs:attributeGroup name="frame-options.attlist">
<xs:attribute name="policy">
<xs:annotation>
<xs:documentation>Specify the policy to use for the X-Frame-Options-Header.
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="DENY"/>
<xs:enumeration value="SAMEORIGIN"/>
<xs:enumeration value="ALLOW-FROM"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="origin" type="xs:token">
<xs:annotation>
<xs:documentation>Specify the origin to use when ALLOW-FROM is chosen.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="xss-protection">
<xs:annotation>
<xs:documentation>Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the
X-XSS-Protection header.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attributeGroup ref="security:xss-protection.attlist"/>
</xs:complexType>
</xs:element>
<xs:attributeGroup name="xss-protection.attlist">
<xs:attribute name="enabled" type="xs:boolean">
<xs:annotation>
<xs:documentation>enable or disable the X-XSS-Protection header. Default is 'true' meaning it is enabled.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="block" type="xs:boolean">
<xs:annotation>
<xs:documentation>Add mode=block to the header or not, default is on.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="content-type-options">
<xs:annotation>
<xs:documentation>Add a X-Content-Type-Options header to the resopnse. Value is always 'nosniff'.
</xs:documentation>
</xs:annotation>
<xs:complexType/>
</xs:element>
<xs:element name="header">
<xs:annotation>
<xs:documentation>Add additional headers to the response.
</xs:documentation>
</xs:annotation>
<xs:complexType>
<xs:attributeGroup ref="security:header.attlist"/>
</xs:complexType>
</xs:element>
<xs:attributeGroup name="header.attlist">
<xs:attribute name="name" use="required" type="xs:token">
<xs:annotation>
<xs:documentation>The name of the header to add.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="value" use="required" type="xs:token">
<xs:annotation>
<xs:documentation>The value for the header.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="any-user-service" abstract="true"/>
<xs:element name="custom-filter">
<xs:annotation>
@@ -9,7 +9,7 @@
<xsl:output method="xml" indent="yes"/>
<xsl:variable name="elts-to-inline">
<xsl:text>,access-denied-handler,anonymous,session-management,concurrency-control,after-invocation-provider,authentication-provider,ldap-authentication-provider,user,port-mapping,openid-login,expression-handler,form-login,http-basic,intercept-url,logout,password-encoder,port-mappings,port-mapper,password-compare,protect,protect-pointcut,pre-post-annotation-handling,pre-invocation-advice,post-invocation-advice,invocation-attribute-factory,remember-me,salt-source,x509,</xsl:text>
<xsl:text>,access-denied-handler,anonymous,session-management,concurrency-control,after-invocation-provider,authentication-provider,ldap-authentication-provider,user,port-mapping,openid-login,expression-handler,form-login,http-basic,intercept-url,logout,password-encoder,port-mappings,port-mapper,password-compare,protect,protect-pointcut,pre-post-annotation-handling,pre-invocation-advice,post-invocation-advice,invocation-attribute-factory,remember-me,salt-source,x509,add-headers,</xsl:text>
</xsl:variable>
<xsl:template match="xs:element">
@@ -48,7 +48,7 @@ public class SecurityNamespaceHandlerTests {
}
@Test
public void pre31SchemaAreNotSupported() throws Exception {
public void pre32SchemaAreNotSupported() throws Exception {
try {
new InMemoryXmlApplicationContext(
"<user-service id='us'>" +
@@ -57,7 +57,7 @@ public class SecurityNamespaceHandlerTests {
);
fail("Expected BeanDefinitionParsingException");
} catch (BeanDefinitionParsingException expected) {
assertTrue(expected.getMessage().contains("You cannot use a spring-security-2.0.xsd or"));
assertTrue(expected.getMessage().contains("You cannot use a spring-security-2.0.xsd"));
}
}