- 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:
+87
-25
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -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
|
||||
|
||||
|
||||
+36
-4
@@ -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">
|
||||
|
||||
+9
-4
@@ -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".'
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user