- 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:
parent
a63baa8391
commit
d0b40cd2ae
|
@ -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".'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -319,9 +319,44 @@
|
|||
including it in a frame it is the same as the one serving the page.
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="nsa-frame-options-origin">
|
||||
<title><literal>frame-options-origin</literal></title>
|
||||
<para>The origin</para>
|
||||
<section xml:id="nsa=frame-options-strategy">
|
||||
<title><literal>frame-options-strategy</literal></title>
|
||||
<para>
|
||||
Select the <classname>AllowFromStrategy</classname> to use when using the ALLOW-FROM policy.
|
||||
<itemizedlist>
|
||||
<listitem><literal>static</literal> Use a single static ALLOW-FROM value. The value can be set
|
||||
through the <link xlink:href="#nsa-frame-options-value">value</link> attribute.
|
||||
</listitem>
|
||||
<listitem><literal>regexp</literal> Use a regelur expression to validate incoming requests and
|
||||
if they are allowed. The regular expression can be set through the <link xlink:href="#nsa-frame-options-value">value</link>
|
||||
attribute. The request parameter used to retrieve the value to validate can be specified
|
||||
using the <link xlink:href="#nsa-frame-options-from-parameter">from-parameter</link>.
|
||||
</listitem>
|
||||
<listitem><literal>whitelist</literal>A comma-seperated list containing the allowed domains.
|
||||
The comma-seperated list can be set through the <link xlink:href="#nsa-frame-options-value">value</link>
|
||||
attribute. The request parameter used to retrieve the value to validate can be specified
|
||||
using the <link xlink:href="#nsa-frame-options-from-parameter">from-parameter</link>.
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="nsa-frame-options-ref">
|
||||
<title><literal>frame-options-ref</literal></title>
|
||||
<para>
|
||||
Instead of using one of the predefined strategies it is also possible to use a custom <classname>AllowFromStrategy</classname>.
|
||||
The reference to this bean can be specified through this ref attribute.
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="nsa-frame-options-value">
|
||||
<title><literal>frame-options-value</literal></title>
|
||||
<para>The value to use when ALLOW-FROM is used a <link xlink:href="#nsa-frame-options-strategy">strategy</link>.</para>
|
||||
</section>
|
||||
<section xml:id="nsa-frame-options-from-parameter">
|
||||
<title><literal>frame-options-from-parameter</literal></title>
|
||||
<para>
|
||||
Specify the name of the request parameter to use when using regexp or whitelist for the ALLOW-FROM
|
||||
strategy.
|
||||
</para>
|
||||
</section>
|
||||
</section>
|
||||
<section xml:id="nsa-frame-options-parents">
|
||||
|
@ -381,6 +416,10 @@
|
|||
<title><literal>header-value</literal></title>
|
||||
<para>The <literal>value</literal> of the header to add.</para>
|
||||
</section>
|
||||
<section xml:id="nsa-header-ref">
|
||||
<title><literal>header-ref</literal></title>
|
||||
<para>Reference to a custom implementation of the <classname>HeaderFactory</classname> interface.</para>
|
||||
</section>
|
||||
</section>
|
||||
<section xml:id="nsa-header-parents">
|
||||
<title>Parent Elements of <literal><header></literal></title>
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
package org.springframework.security.web.headers;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* Created with IntelliJ IDEA.
|
||||
* User: marten
|
||||
* Date: 29-01-13
|
||||
* Time: 20:26
|
||||
* To change this template use File | Settings | File Templates.
|
||||
*/
|
||||
public final class Header {
|
||||
|
||||
private final String name;
|
||||
private final String[] values;
|
||||
|
||||
public Header(String name, String... values) {
|
||||
this.name = name;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String[] getValues() {
|
||||
return this.values;
|
||||
}
|
||||
|
||||
public int hashCode() {
|
||||
return name.hashCode() + Arrays.hashCode(values);
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return "Header [name: " + name + ", values: " + Arrays.toString(values)+"]";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package org.springframework.security.web.headers;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Contract for a factory that creates {@code Header} instances.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
* @see HeadersFilter
|
||||
*/
|
||||
public interface HeaderFactory {
|
||||
|
||||
/**
|
||||
* Create a {@code Header} instance.
|
||||
*
|
||||
* @param request the request
|
||||
* @param response the response
|
||||
* @return the created Header or <code>null</code>
|
||||
*/
|
||||
Header create(HttpServletRequest request, HttpServletResponse response);
|
||||
}
|
|
@ -22,8 +22,7 @@ import javax.servlet.ServletException;
|
|||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Filter implementation to add headers to the current request. Can be useful to add certain headers which enable
|
||||
|
@ -35,28 +34,41 @@ import java.util.Map;
|
|||
*/
|
||||
public class HeadersFilter extends OncePerRequestFilter {
|
||||
|
||||
/** Map of headers to add to a response */
|
||||
private final Map<String, String> headers = new HashMap<String, String>();
|
||||
/** Collection of HeaderFactory instances to produce Headers. */
|
||||
private final List<HeaderFactory> factories;
|
||||
|
||||
public HeadersFilter(List<HeaderFactory> factories) {
|
||||
this.factories = factories;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
String name = header.getKey();
|
||||
String value = header.getValue();
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Adding header '" + name + "' with value '"+value +"'");
|
||||
|
||||
for (HeaderFactory factory : factories) {
|
||||
Header header = factory.create(request, response);
|
||||
if (header != null) {
|
||||
String name = header.getName();
|
||||
String[] values = header.getValues();
|
||||
boolean first = true;
|
||||
for (String value : values) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Adding header '" + name + "' with value '"+value +"'");
|
||||
}
|
||||
if (first) {
|
||||
response.setHeader(name, value);
|
||||
first = false;
|
||||
} else {
|
||||
response.addHeader(name, value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Factory produced no header.");
|
||||
}
|
||||
}
|
||||
response.setHeader(header.getKey(), header.getValue());
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
public void setHeaders(Map<String, String> headers) {
|
||||
this.headers.clear();
|
||||
this.headers.putAll(headers);
|
||||
}
|
||||
|
||||
public void addHeader(String name, String value) {
|
||||
headers.put(name, value);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package org.springframework.security.web.headers;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* {@code HeaderFactory} implementation which returns the same {@code Header} instance.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
*/
|
||||
public class StaticHeaderFactory implements HeaderFactory {
|
||||
|
||||
private final Header header;
|
||||
|
||||
public StaticHeaderFactory(String name, String... values) {
|
||||
header = new Header(name, values);
|
||||
}
|
||||
|
||||
public Header create(HttpServletRequest request, HttpServletResponse response) {
|
||||
return header;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Strategy interfaces used by the {@code FrameOptionsHeaderFactory} to determine the actual value to use for the
|
||||
* X-Frame-Options header when using the ALLOW-FROM directive.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
*/
|
||||
public interface AllowFromStrategy {
|
||||
|
||||
String apply(HttpServletRequest request);
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import org.springframework.security.web.headers.Header;
|
||||
import org.springframework.security.web.headers.HeaderFactory;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* {@code HeaderFactory} implementation for the X-Frame-Options headers. When using the ALLOW-FROM directive the actual
|
||||
* value is determined by a {@code AllowFromStrategy}.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
*
|
||||
* @see AllowFromStrategy
|
||||
*/
|
||||
public class FrameOptionsHeaderFactory implements HeaderFactory {
|
||||
|
||||
public static final String FRAME_OPTIONS_HEADER = "X-Frame-Options";
|
||||
|
||||
private static final String ALLOW_FROM = "ALLOW-FROM";
|
||||
|
||||
private final AllowFromStrategy allowFromStrategy;
|
||||
private final String mode;
|
||||
|
||||
public FrameOptionsHeaderFactory(String mode) {
|
||||
this(mode, new NullAllowFromStrategy());
|
||||
}
|
||||
|
||||
public FrameOptionsHeaderFactory(String mode, AllowFromStrategy allowFromStrategy) {
|
||||
this.mode=mode;
|
||||
this.allowFromStrategy=allowFromStrategy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Header create(HttpServletRequest request, HttpServletResponse response) {
|
||||
if (ALLOW_FROM.equals(mode)) {
|
||||
String value = allowFromStrategy.apply(request);
|
||||
return new Header(FRAME_OPTIONS_HEADER, value);
|
||||
} else {
|
||||
return new Header(FRAME_OPTIONS_HEADER, mode);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Created with IntelliJ IDEA.
|
||||
* User: marten
|
||||
* Date: 30-01-13
|
||||
* Time: 11:06
|
||||
* To change this template use File | Settings | File Templates.
|
||||
*/
|
||||
public class NullAllowFromStrategy implements AllowFromStrategy {
|
||||
@Override
|
||||
public String apply(HttpServletRequest request) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Implementation which uses a regular expression to validate the supplied origin.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
*/
|
||||
public class RegExpAllowFromStrategy extends RequestParameterAllowFromStrategy {
|
||||
|
||||
private final Pattern pattern;
|
||||
|
||||
public RegExpAllowFromStrategy(String pattern) {
|
||||
Assert.hasText(pattern, "Pattern cannot be empty.");
|
||||
this.pattern = Pattern.compile(pattern);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean allowed(String from) {
|
||||
return pattern.matcher(from).matches();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Base class for AllowFromStrategy implementations which use a request parameter to retrieve the origin. By default
|
||||
* the parameter named <code>from</code> is read from the request.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
*/
|
||||
public abstract class RequestParameterAllowFromStrategy implements AllowFromStrategy {
|
||||
|
||||
|
||||
private static final String DEFAULT_ORIGIN_REQUEST_PARAMETER = "from";
|
||||
|
||||
private String parameter = DEFAULT_ORIGIN_REQUEST_PARAMETER;
|
||||
|
||||
/** Logger for use by subclasses */
|
||||
protected final Log log = LogFactory.getLog(getClass());
|
||||
|
||||
|
||||
@Override
|
||||
public String apply(HttpServletRequest request) {
|
||||
String from = request.getParameter(parameter);
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Supplied origin '"+from+"'");
|
||||
}
|
||||
if (StringUtils.hasText(from) && allowed(from)) {
|
||||
return "ALLOW-FROM " + from;
|
||||
} else {
|
||||
return "DENY";
|
||||
}
|
||||
}
|
||||
|
||||
public void setParameterName(String parameter) {
|
||||
this.parameter=parameter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to be implemented by base classes, used to determine if the supplied origin is allowed.
|
||||
*
|
||||
* @param from the supplied origin
|
||||
* @return <code>true</code> if the supplied origin is allowed.
|
||||
*/
|
||||
protected abstract boolean allowed(String from);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* Simple implementation of the {@code AllowFromStrategy}
|
||||
*/
|
||||
public class StaticAllowFromStrategy implements AllowFromStrategy {
|
||||
|
||||
private final URI uri;
|
||||
|
||||
public StaticAllowFromStrategy(URI uri) {
|
||||
this.uri=uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apply(HttpServletRequest request) {
|
||||
return uri.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Implementation which checks the supplied origin against a list of allowed origins.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
*/
|
||||
public class WhiteListedAllowFromStrategy extends RequestParameterAllowFromStrategy {
|
||||
|
||||
private final Collection<String> allowed;
|
||||
|
||||
public WhiteListedAllowFromStrategy(Collection<String> allowed) {
|
||||
Assert.notEmpty(allowed, "Allowed origins cannot be empty.");
|
||||
this.allowed = allowed;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean allowed(String from) {
|
||||
return allowed.contains(from);
|
||||
}
|
||||
}
|
|
@ -20,9 +20,9 @@ import org.springframework.mock.web.MockFilterChain;
|
|||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.*;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
@ -39,7 +39,8 @@ public class HeadersFilterTest {
|
|||
|
||||
@Test
|
||||
public void noHeadersConfigured() throws Exception {
|
||||
HeadersFilter filter = new HeadersFilter();
|
||||
List<HeaderFactory> factories = new ArrayList();
|
||||
HeadersFilter filter = new HeadersFilter(factories);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
MockFilterChain filterChain = new MockFilterChain();
|
||||
|
@ -51,11 +52,18 @@ public class HeadersFilterTest {
|
|||
|
||||
@Test
|
||||
public void additionalHeadersShouldBeAddedToTheResponse() throws Exception {
|
||||
HeadersFilter filter = new HeadersFilter();
|
||||
Map<String, String> headers = new HashMap<String, String>();
|
||||
headers.put("X-Header1", "foo");
|
||||
headers.put("X-Header2", "bar");
|
||||
filter.setHeaders(headers);
|
||||
List<HeaderFactory> factories = new ArrayList();
|
||||
MockHeaderFactory factory1 = new MockHeaderFactory();
|
||||
factory1.setName("X-Header1");
|
||||
factory1.setValue("foo");
|
||||
MockHeaderFactory factory2 = new MockHeaderFactory();
|
||||
factory2.setName("X-Header2");
|
||||
factory2.setValue("bar");
|
||||
|
||||
factories.add(factory1);
|
||||
factories.add(factory2);
|
||||
|
||||
HeadersFilter filter = new HeadersFilter(factories);
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
@ -70,4 +78,24 @@ public class HeadersFilterTest {
|
|||
assertThat(response.getHeader("X-Header2"), is("bar"));
|
||||
|
||||
}
|
||||
|
||||
private static final class MockHeaderFactory implements HeaderFactory {
|
||||
|
||||
private String name;
|
||||
private String value;
|
||||
|
||||
@Override
|
||||
public Header create(HttpServletRequest request, HttpServletResponse response) {
|
||||
return new Header(name, value);
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name=name;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value=value;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package org.springframework.security.web.headers;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.springframework.test.util.MatcherAssertionErrors.assertThat;
|
||||
|
||||
/**
|
||||
* Test for the {@code StaticHeaderFactory}
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
*/
|
||||
public class StaticHeaderFactoryTest {
|
||||
|
||||
@Test
|
||||
public void sameHeaderShouldBeReturned() {
|
||||
StaticHeaderFactory factory = new StaticHeaderFactory("X-header", "foo");
|
||||
Header header = factory.create(null, null);
|
||||
assertThat(header.getName(), is("X-header"));
|
||||
assertThat(header.getValues()[0], is("foo"));
|
||||
|
||||
assertSame(header, factory.create(null, null));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* Created with IntelliJ IDEA.
|
||||
* User: marten
|
||||
* Date: 01-02-13
|
||||
* Time: 20:25
|
||||
* To change this template use File | Settings | File Templates.
|
||||
*/
|
||||
public class RegExpAllowFromStrategyTest {
|
||||
|
||||
@Test(expected = PatternSyntaxException.class)
|
||||
public void invalidRegularExpressionShouldLeadToException() {
|
||||
new RegExpAllowFromStrategy("[a-z");
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nullRegularExpressionShouldLeadToException() {
|
||||
new RegExpAllowFromStrategy(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subdomainMatchingRegularExpression() {
|
||||
RegExpAllowFromStrategy strategy = new RegExpAllowFromStrategy("^http://([a-z0-9]*?\\.)test\\.com");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
request.setParameter("from", "http://abc.test.com");
|
||||
String result1 = strategy.apply(request);
|
||||
assertThat(result1, is("ALLOW-FROM http://abc.test.com"));
|
||||
|
||||
request.setParameter("from", "http://foo.test.com");
|
||||
String result2 = strategy.apply(request);
|
||||
assertThat(result2, is("ALLOW-FROM http://foo.test.com"));
|
||||
|
||||
request.setParameter("from", "http://test.foobar.com");
|
||||
String result3 = strategy.apply(request);
|
||||
assertThat(result3, is("DENY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void noParameterShouldDeny() {
|
||||
RegExpAllowFromStrategy strategy = new RegExpAllowFromStrategy("^http://([a-z0-9]*?\\.)test\\.com");
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
String result1 = strategy.apply(request);
|
||||
assertThat(result1, is("DENY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test() {
|
||||
String pattern = "^http://([a-z0-9]*?\\.)test\\.com";
|
||||
Pattern p = Pattern.compile(pattern);
|
||||
String url = "http://abc.test.com";
|
||||
assertTrue(p.matcher(url).matches());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Test for the StaticAllowFromStrategy.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
*/
|
||||
public class StaticAllowFromStrategyTest {
|
||||
|
||||
@Test
|
||||
public void shouldReturnUri() {
|
||||
String uri = "http://www.test.com";
|
||||
StaticAllowFromStrategy strategy = new StaticAllowFromStrategy(URI.create(uri));
|
||||
assertEquals(uri, strategy.apply(new MockHttpServletRequest()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package org.springframework.security.web.headers.frameoptions;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.springframework.test.util.MatcherAssertionErrors.assertThat;
|
||||
|
||||
/**
|
||||
* Test for the {@code WhiteListedAllowFromStrategy}.
|
||||
*
|
||||
* @author Marten Deinum
|
||||
* @since 3.2
|
||||
*/
|
||||
public class WhiteListedAllowFromStrategyTest {
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void emptyListShouldThrowException() {
|
||||
new WhiteListedAllowFromStrategy(new ArrayList<String>());
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void nullListShouldThrowException() {
|
||||
new WhiteListedAllowFromStrategy(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listWithSingleElementShouldMatch() {
|
||||
List<String> allowed = new ArrayList<String>();
|
||||
allowed.add("http://www.test.com");
|
||||
WhiteListedAllowFromStrategy strategy = new WhiteListedAllowFromStrategy(allowed);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter("from", "http://www.test.com");
|
||||
|
||||
String result = strategy.apply(request);
|
||||
assertThat(result, is("ALLOW-FROM http://www.test.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listWithMultipleElementShouldMatch() {
|
||||
List<String> allowed = new ArrayList<String>();
|
||||
allowed.add("http://www.test.com");
|
||||
allowed.add("http://www.springsource.org");
|
||||
WhiteListedAllowFromStrategy strategy = new WhiteListedAllowFromStrategy(allowed);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter("from", "http://www.test.com");
|
||||
|
||||
String result = strategy.apply(request);
|
||||
assertThat(result, is("ALLOW-FROM http://www.test.com"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void listWithSingleElementShouldNotMatch() {
|
||||
List<String> allowed = new ArrayList<String>();
|
||||
allowed.add("http://www.test.com");
|
||||
WhiteListedAllowFromStrategy strategy = new WhiteListedAllowFromStrategy(allowed);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setParameter("from", "http://www.test123.com");
|
||||
|
||||
String result = strategy.apply(request);
|
||||
assertThat(result, is("DENY"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWithoutParameterShouldNotMatch() {
|
||||
List<String> allowed = new ArrayList<String>();
|
||||
allowed.add("http://www.test.com");
|
||||
WhiteListedAllowFromStrategy strategy = new WhiteListedAllowFromStrategy(allowed);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
String result = strategy.apply(request);
|
||||
assertThat(result, is("DENY"));
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue