1
0
mirror of synced 2026-08-05 09:47:05 +00:00

- Created HeaderFactory abstraction

- Implemented different ALLOW-FROM strategies as specified in the proposal.

Conflicts:
	config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java
	config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy
This commit is contained in:
Marten Deinum
2013-02-22 20:43:29 +01:00
committed by Rob Winch
parent a63baa8391
commit d0b40cd2ae
21 changed files with 740 additions and 67 deletions
@@ -16,17 +16,22 @@
package org.springframework.security.config.http;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.web.headers.HeadersFilter;
import org.springframework.security.web.headers.StaticHeaderFactory;
import org.springframework.security.web.headers.frameoptions.*;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import java.util.HashMap;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.regex.PatternSyntaxException;
/**
* Parser for the {@code HeadersFilter}.
@@ -40,10 +45,12 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
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_STRATEGY = "strategy";
private static final String ATT_FROM_PARAMETER = "from-parameter";
private static final String ATT_NAME = "name";
private static final String ATT_VALUE = "value";
private static final String ATT_REF = "ref";
private static final String XSS_ELEMENT = "xss-protection";
private static final String CONTENT_TYPE_ELEMENT = "content-type-options";
@@ -51,55 +58,107 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
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";
private ManagedList headerFactories;
public BeanDefinition parse(Element element, ParserContext parserContext) {
headerFactories = new ManagedList();
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(HeadersFilter.class);
final Map<String, String> headers = new HashMap<String, String>();
parseXssElement(element, parserContext, headers);
parseFrameOptionsElement(element, parserContext, headers);
parseContentTypeOptionsElement(element, headers);
parseXssElement(element, parserContext);
parseFrameOptionsElement(element, parserContext);
parseContentTypeOptionsElement(element);
parseHeaderElements(element, headers);
parseHeaderElements(element);
builder.addPropertyValue("headers", headers);
builder.addConstructorArgValue(headerFactories);
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 parseHeaderElements(Element element) {
List<Element> headerElts = DomUtils.getChildElementsByTagName(element, GENERIC_HEADER_ELEMENT);
for (Element headerElt : headerElts) {
String headerFactoryRef = headerElt.getAttribute(ATT_REF);
if (StringUtils.hasText(headerFactoryRef)) {
headerFactories.add(new RuntimeBeanReference(headerFactoryRef));
} else {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(StaticHeaderFactory.class);
builder.addConstructorArgValue(headerElt.getAttribute(ATT_NAME));
builder.addConstructorArgValue(headerElt.getAttribute(ATT_VALUE));
headerFactories.add(builder.getBeanDefinition());
}
}
}
private void parseContentTypeOptionsElement(Element element, Map<String, String> headers) {
private void parseContentTypeOptionsElement(Element element) {
Element contentTypeElt = DomUtils.getChildElementByTagName(element, CONTENT_TYPE_ELEMENT);
if (contentTypeElt != null) {
headers.put(CONTENT_TYPE_OPTIONS_HEADER, "nosniff");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(StaticHeaderFactory.class);
builder.addConstructorArgValue(CONTENT_TYPE_OPTIONS_HEADER);
builder.addConstructorArgValue("nosniff");
headerFactories.add(builder.getBeanDefinition());
}
}
private void parseFrameOptionsElement(Element element, ParserContext parserContext, Map<String, String> headers) {
private void parseFrameOptionsElement(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FrameOptionsHeaderFactory.class);
Element frameElt = DomUtils.getChildElementByTagName(element, FRAME_OPTIONS_ELEMENT);
if (frameElt != null) {
String header = getAttribute(frameElt, ATT_POLICY, "DENY");
builder.addConstructorArgValue(header);
if (ALLOW_FROM.equals(header) ) {
String origin = frameElt.getAttribute(ATT_ORIGIN);
if (!StringUtils.hasText(origin) ) {
parserContext.getReaderContext().error("<frame-options policy=\"ALLOW-FROM\"/> requires a non-empty string value for the origin attribute to be specified.", frameElt);
String strategyRef = getAttribute(frameElt, ATT_REF, null);
String strategy = getAttribute(frameElt, ATT_STRATEGY, null);
if (StringUtils.hasText(strategy) && StringUtils.hasText(strategyRef)) {
parserContext.getReaderContext().error("Only one of 'strategy' or 'strategy-ref' can be set.",
frameElt);
} else if (strategyRef != null) {
builder.addConstructorArgReference(strategyRef);
} else if (strategy != null) {
String value = getAttribute(frameElt, ATT_VALUE, null);
if (!StringUtils.hasText(value)) {
parserContext.getReaderContext().error("Strategy requires a 'value' to be set.", frameElt);
}
// static, whitelist, regexp
if ("static".equals(strategy)) {
try {
builder.addConstructorArgValue(new StaticAllowFromStrategy(new URI(value)));
} catch (URISyntaxException e) {
parserContext.getReaderContext().error(
"'value' attribute doesn't represent a valid URI.", frameElt, e);
}
} else {
RequestParameterAllowFromStrategy allowFromStrategy = null;
if ("whitelist".equals(strategy)) {
allowFromStrategy = new WhiteListedAllowFromStrategy(
StringUtils.commaDelimitedListToSet(value));
} else {
try {
allowFromStrategy = new RegExpAllowFromStrategy(value);
} catch (PatternSyntaxException e) {
parserContext.getReaderContext().error(
"'value' attribute doesn't represent a valid regular expression.", frameElt, e);
}
}
String fromParameter = getAttribute(frameElt, ATT_FROM_PARAMETER, "from");
allowFromStrategy.setParameterName(fromParameter);
builder.addConstructorArgValue(allowFromStrategy);
}
} else {
parserContext.getReaderContext().error("One of 'strategy' and 'strategy-ref' must be set.",
frameElt);
}
header += " " + origin;
}
headers.put(FRAME_OPTIONS_HEADER, header);
headerFactories.add(builder.getBeanDefinition());
}
}
private void parseXssElement(Element element, ParserContext parserContext, Map<String, String> headers) {
private void parseXssElement(Element element, ParserContext parserContext) {
Element xssElt = DomUtils.getChildElementByTagName(element, XSS_ELEMENT);
if (xssElt != null) {
boolean enabled = Boolean.valueOf(getAttribute(xssElt, ATT_ENABLED, "true"));
@@ -109,9 +168,12 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
if (enabled && block) {
value += "; mode=block";
} else if (!enabled && block) {
parserContext.getReaderContext().error("<xss-protection enabled=\"false\"/> does not allow for the block=\"true\".", xssElt);
parserContext.getReaderContext().error("<xss-protection enabled=\"false\"/> does not allow block=\"true\".", xssElt);
}
headers.put(XSS_PROTECTION_HEADER, value);
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(StaticHeaderFactory.class);
builder.addConstructorArgValue(XSS_PROTECTION_HEADER);
builder.addConstructorArgValue(value);
headerFactories.add(builder.getBeanDefinition());
}
}
@@ -729,8 +729,18 @@ 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}?
## Specify the strategy to use when ALLOW-FROM is chosen.
attribute strategy {"static","whitelist","regexp"}?
frame-options.attlist &=
## Specify the a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen.
ref?
frame-options.attlist &=
## Specify the a value to use for the chosen strategy.
attribute value {xsd:string}?
frame-options.attlist &=
## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'.
attribute from-parameter {xsd:string}?
xss-protection =
## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header.
@@ -751,10 +761,13 @@ header=
element header {header.attlist}
header.attlist &=
## The name of the header to add.
attribute name {xsd:token}
attribute name {xsd:token}?
header.attlist &=
## The value for the header.
attribute value {xsd:token}
attribute value {xsd:token}?
header.attlist &=
## Reference to a custom HeaderFactory implementation.
ref?
any-user-service = user-service | jdbc-user-service | ldap-user-service
@@ -2271,9 +2271,35 @@
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="origin" type="xs:token">
<xs:attribute name="strategy">
<xs:annotation>
<xs:documentation>Specify the origin to use when ALLOW-FROM is chosen.
<xs:documentation>Specify the strategy to use when ALLOW-FROM is chosen.
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="static"/>
<xs:enumeration value="whitelist"/>
<xs:enumeration value="regexp"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="ref" type="xs:token">
<xs:annotation>
<xs:documentation>Defines a reference to a Spring bean Id.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="value" type="xs:string">
<xs:annotation>
<xs:documentation>Specify the a value to use for the chosen strategy.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="from-parameter" type="xs:string">
<xs:annotation>
<xs:documentation>Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp'
based strategy. Default is 'from'.
</xs:documentation>
</xs:annotation>
</xs:attribute>
@@ -2319,18 +2345,24 @@
</xs:complexType>
</xs:element>
<xs:attributeGroup name="header.attlist">
<xs:attribute name="name" use="required" type="xs:token">
<xs:attribute name="name" 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:attribute name="value" type="xs:token">
<xs:annotation>
<xs:documentation>The value for the header.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ref" type="xs:token">
<xs:annotation>
<xs:documentation>Defines a reference to a Spring bean Id.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="any-user-service" abstract="true"/>
<xs:element name="custom-filter">
@@ -12,6 +12,8 @@
*/
package org.springframework.security.config.http
import org.springframework.security.util.FieldUtils
import javax.servlet.Filter
import javax.servlet.http.HttpServletRequest
@@ -54,10 +56,12 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
createAppContext()
def hf = getFilter(HeadersFilter)
MockHttpServletResponse response = new MockHttpServletResponse();
hf.doFilter(new MockHttpServletRequest(), response);
expect:
hf
hf.headers.isEmpty()
response.headers.isEmpty()
}
def 'http headers content-type-options'() {
@@ -69,10 +73,11 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
createAppContext()
def hf = getFilter(HeadersFilter)
MockHttpServletResponse response = new MockHttpServletResponse();
hf.doFilter(new MockHttpServletRequest(), response);
expect:
hf
hf.headers == ['X-Content-Type-Options':'nosniff']
response.headers == ['X-Content-Type-Options':'nosniff']
}
def 'http headers frame-options defaults to DENY'() {
@@ -288,6 +293,6 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
then:
BeanDefinitionParsingException e = thrown()
e.message.contains '<xss-protection enabled="false"/> does not allow for the block="true".'
e.message.contains '<xss-protection enabled="false"/> does not allow block="true".'
}
}