/*
* Copyright 2002-2016 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 java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeanMetadataElement;
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.header.HeaderWriterFilter;
import org.springframework.security.web.header.writers.*;
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter.ReferrerPolicy;
import org.springframework.security.web.header.writers.frameoptions.RegExpAllowFromStrategy;
import org.springframework.security.web.header.writers.frameoptions.StaticAllowFromStrategy;
import org.springframework.security.web.header.writers.frameoptions.WhiteListedAllowFromStrategy;
import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
/**
* Parser for the {@code HeadersFilter}.
*
* @author Marten Deinum
* @author Tim Ysewyn
* @author EddĂș MelĂ©ndez
* @since 3.2
*/
public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
private static final String ATT_DISABLED = "disabled";
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_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 ATT_INCLUDE_SUBDOMAINS = "include-subdomains";
private static final String ATT_MAX_AGE_SECONDS = "max-age-seconds";
private static final String ATT_REQUEST_MATCHER_REF = "request-matcher-ref";
private static final String ATT_REPORT_ONLY = "report-only";
private static final String ATT_REPORT_URI = "report-uri";
private static final String ATT_ALGORITHM = "algorithm";
private static final String ATT_POLICY_DIRECTIVES = "policy-directives";
private static final String CACHE_CONTROL_ELEMENT = "cache-control";
private static final String HPKP_ELEMENT = "hpkp";
private static final String PINS_ELEMENT = "pins";
private static final String HSTS_ELEMENT = "hsts";
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 CONTENT_SECURITY_POLICY_ELEMENT = "content-security-policy";
private static final String REFERRER_POLICY_ELEMENT = "referrer-policy";
private static final String ALLOW_FROM = "ALLOW-FROM";
private ManagedList<BeanMetadataElement> headerWriters;
public BeanDefinition parse(Element element, ParserContext parserContext) {
headerWriters = new ManagedList<BeanMetadataElement>();
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.rootBeanDefinition(HeaderWriterFilter.class);
boolean disabled = element != null
&& "true".equals(element.getAttribute("disabled"));
boolean defaultsDisabled = element != null
&& "true".equals(element.getAttribute("defaults-disabled"));
boolean addIfNotPresent = element == null || !disabled && !defaultsDisabled;
parseCacheControlElement(addIfNotPresent, element);
parseHstsElement(addIfNotPresent, element, parserContext);
parseXssElement(addIfNotPresent, element, parserContext);
parseFrameOptionsElement(addIfNotPresent, element, parserContext);
parseContentTypeOptionsElement(addIfNotPresent, element);
parseHpkpElement(element == null || !disabled, element, parserContext);
parseContentSecurityPolicyElement(disabled, element, parserContext);
parseReferrerPolicyElement(element, parserContext);
parseHeaderElements(element);
boolean noWriters = headerWriters.isEmpty();
if (disabled && !noWriters) {
parserContext
.getReaderContext()
.error("Cannot specify <headers disabled=\"true\"> with child elements.",
element);
} else if (noWriters) {
return null;
}
builder.addConstructorArgValue(headerWriters);
return builder.getBeanDefinition();
}
private void parseCacheControlElement(boolean addIfNotPresent, Element element) {
Element cacheControlElement = element == null ? null : DomUtils
.getChildElementByTagName(element, CACHE_CONTROL_ELEMENT);
boolean disabled = "true".equals(getAttribute(cacheControlElement, ATT_DISABLED,
"false"));
if (disabled) {
return;
}
if (addIfNotPresent || cacheControlElement != null) {
addCacheControl();
}
}
private void addCacheControl() {
BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder
.genericBeanDefinition(CacheControlHeadersWriter.class);
headerWriters.add(headersWriter.getBeanDefinition());
}
private void parseHstsElement(boolean addIfNotPresent, Element element,
ParserContext context) {
Element hstsElement = element == null ? null : DomUtils.getChildElementByTagName(
element, HSTS_ELEMENT);
if (addIfNotPresent || hstsElement != null) {
addHsts(addIfNotPresent, hstsElement, context);
}
}
private void addHsts(boolean addIfNotPresent, Element hstsElement,
ParserContext context) {
BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder
.genericBeanDefinition(HstsHeaderWriter.class);
if (hstsElement != null) {
boolean disabled = "true".equals(getAttribute(hstsElement, ATT_DISABLED,
"false"));
String includeSubDomains = hstsElement.getAttribute(ATT_INCLUDE_SUBDOMAINS);
if (StringUtils.hasText(includeSubDomains)) {
if (disabled) {
attrNotAllowed(context, ATT_INCLUDE_SUBDOMAINS, ATT_DISABLED,
hstsElement);
}
headersWriter.addPropertyValue("includeSubDomains", includeSubDomains);
}
String maxAgeSeconds = hstsElement.getAttribute(ATT_MAX_AGE_SECONDS);
if (StringUtils.hasText(maxAgeSeconds)) {
if (disabled) {
attrNotAllowed(context, ATT_MAX_AGE_SECONDS, ATT_DISABLED,
hstsElement);
}
headersWriter.addPropertyValue("maxAgeInSeconds", maxAgeSeconds);
}
String requestMatcherRef = hstsElement.getAttribute(ATT_REQUEST_MATCHER_REF);
if (StringUtils.hasText(requestMatcherRef)) {
if (disabled) {
attrNotAllowed(context, ATT_REQUEST_MATCHER_REF, ATT_DISABLED,
hstsElement);
}
headersWriter.addPropertyReference("requestMatcher", requestMatcherRef);
}
if (disabled == true) {
return;
}
}
if (addIfNotPresent || hstsElement != null) {
headerWriters.add(headersWriter.getBeanDefinition());
}
}
private void parseHpkpElement(boolean addIfNotPresent, Element element, ParserContext context) {
Element hpkpElement = element == null ? null : DomUtils.getChildElementByTagName(element, HPKP_ELEMENT);
if (addIfNotPresent || hpkpElement != null) {
addHpkp(addIfNotPresent, hpkpElement, context);
}
}
private void addHpkp(boolean addIfNotPresent, Element hpkpElement, ParserContext context) {
if (hpkpElement != null) {
boolean disabled = "true".equals(getAttribute(hpkpElement, ATT_DISABLED, "false"));
if (disabled) {
return;
}
BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder.genericBeanDefinition(HpkpHeaderWriter.class);
Element pinsElement = DomUtils.getChildElementByTagName(hpkpElement, PINS_ELEMENT);
if (pinsElement != null) {
List<Element> pinElements = DomUtils.getChildElements(pinsElement);
Map<String, String> pins = new LinkedHashMap<String, String>();
for (Element pinElement : pinElements) {
String hash = pinElement.getAttribute(ATT_ALGORITHM);
if (!StringUtils.hasText(hash)) {
hash = "sha256";
}
Node pinValueNode = pinElement.getFirstChild();
if (pinValueNode == null) {
context.getReaderContext().warning("Missing value for pin entry.", hpkpElement);
continue;
}
String fingerprint = pinElement.getFirstChild().getTextContent();
pins.put(fingerprint, hash);
}
headersWriter.addPropertyValue("pins", pins);
}
String includeSubDomains = hpkpElement.getAttribute(ATT_INCLUDE_SUBDOMAINS);
if (StringUtils.hasText(includeSubDomains)) {
headersWriter.addPropertyValue("includeSubDomains", includeSubDomains);
}
String maxAgeSeconds = hpkpElement.getAttribute(ATT_MAX_AGE_SECONDS);
if (StringUtils.hasText(maxAgeSeconds)) {
headersWriter.addPropertyValue("maxAgeInSeconds", maxAgeSeconds);
}
String reportOnly = hpkpElement.getAttribute(ATT_REPORT_ONLY);
if (StringUtils.hasText(reportOnly)) {
headersWriter.addPropertyValue("reportOnly", reportOnly);
}
String reportUri = hpkpElement.getAttribute(ATT_REPORT_URI);
if (StringUtils.hasText(reportUri)) {
headersWriter.addPropertyValue("reportUri", reportUri);
}
if (addIfNotPresent) {
headerWriters.add(headersWriter.getBeanDefinition());
}
}
}
private void parseContentSecurityPolicyElement(boolean elementDisabled, Element element, ParserContext context) {
Element contentSecurityPolicyElement = (elementDisabled || element == null) ? null : DomUtils.getChildElementByTagName(
element, CONTENT_SECURITY_POLICY_ELEMENT);
if (contentSecurityPolicyElement != null) {
addContentSecurityPolicy(contentSecurityPolicyElement, context);
}
}
private void addContentSecurityPolicy(Element contentSecurityPolicyElement, ParserContext context) {
BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder
.genericBeanDefinition(ContentSecurityPolicyHeaderWriter.class);
String policyDirectives = contentSecurityPolicyElement.getAttribute(ATT_POLICY_DIRECTIVES);
if (!StringUtils.hasText(policyDirectives)) {
context.getReaderContext().error(
ATT_POLICY_DIRECTIVES + " requires a 'value' to be set.", contentSecurityPolicyElement);
} else {
headersWriter.addConstructorArgValue(policyDirectives);
}
String reportOnly = contentSecurityPolicyElement.getAttribute(ATT_REPORT_ONLY);
if (StringUtils.hasText(reportOnly)) {
headersWriter.addPropertyValue("reportOnly", reportOnly);
}
headerWriters.add(headersWriter.getBeanDefinition());
}
private void parseReferrerPolicyElement(Element element, ParserContext context) {
Element referrerPolicyElement = (element == null) ? null : DomUtils.getChildElementByTagName(element, REFERRER_POLICY_ELEMENT);
if (referrerPolicyElement != null) {
addReferrerPolicy(referrerPolicyElement, context);
}
}
private void addReferrerPolicy(Element referrerPolicyElement, ParserContext context) {
BeanDefinitionBuilder headersWriter = BeanDefinitionBuilder.genericBeanDefinition(ReferrerPolicyHeaderWriter.class);
String policy = referrerPolicyElement.getAttribute(ATT_POLICY);
if (StringUtils.hasLength(policy)) {
headersWriter.addConstructorArgValue(ReferrerPolicy.get(policy));
}
headerWriters.add(headersWriter.getBeanDefinition());
}
private void attrNotAllowed(ParserContext context, String attrName,
String otherAttrName, Element element) {
context.getReaderContext().error(
"Only one of '" + attrName + "' or '" + otherAttrName + "' can be set.",
element);
}
private void parseHeaderElements(Element element) {
List<Element> headerElts = element == null ? Collections.<Element> emptyList()
: DomUtils.getChildElementsByTagName(element, GENERIC_HEADER_ELEMENT);
for (Element headerElt : headerElts) {
String headerFactoryRef = headerElt.getAttribute(ATT_REF);
if (StringUtils.hasText(headerFactoryRef)) {
headerWriters.add(new RuntimeBeanReference(headerFactoryRef));
}
else {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(StaticHeadersWriter.class);
builder.addConstructorArgValue(headerElt.getAttribute(ATT_NAME));
builder.addConstructorArgValue(headerElt.getAttribute(ATT_VALUE));
headerWriters.add(builder.getBeanDefinition());
}
}
}
private void parseContentTypeOptionsElement(boolean addIfNotPresent, Element element) {
Element contentTypeElt = element == null ? null : DomUtils
.getChildElementByTagName(element, CONTENT_TYPE_ELEMENT);
boolean disabled = "true".equals(getAttribute(contentTypeElt, ATT_DISABLED,
"false"));
if (disabled) {
return;
}
if (addIfNotPresent || contentTypeElt != null) {
addContentTypeOptions();
}
}
private void addContentTypeOptions() {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(XContentTypeOptionsHeaderWriter.class);
headerWriters.add(builder.getBeanDefinition());
}
private void parseFrameOptionsElement(boolean addIfNotPresent, Element element,
ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(XFrameOptionsHeaderWriter.class);
Element frameElt = element == null ? null : DomUtils.getChildElementByTagName(
element, FRAME_OPTIONS_ELEMENT);
if (frameElt != null) {
String header = getAttribute(frameElt, ATT_POLICY, null);
boolean disabled = "true"
.equals(getAttribute(frameElt, ATT_DISABLED, "false"));
if (disabled && header != null) {
this.attrNotAllowed(parserContext, ATT_DISABLED, ATT_POLICY, frameElt);
}
if (!StringUtils.hasText(header)) {
header = "DENY";
}
if (ALLOW_FROM.equals(header)) {
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 {
BeanDefinitionBuilder allowFromStrategy;
if ("whitelist".equals(strategy)) {
allowFromStrategy = BeanDefinitionBuilder
.rootBeanDefinition(WhiteListedAllowFromStrategy.class);
allowFromStrategy.addConstructorArgValue(StringUtils
.commaDelimitedListToSet(value));
}
else {
allowFromStrategy = BeanDefinitionBuilder
.rootBeanDefinition(RegExpAllowFromStrategy.class);
allowFromStrategy.addConstructorArgValue(value);
}
String fromParameter = getAttribute(frameElt, ATT_FROM_PARAMETER,
"from");
allowFromStrategy.addPropertyValue("allowFromParameterName",
fromParameter);
builder.addConstructorArgValue(allowFromStrategy
.getBeanDefinition());
}
}
else {
parserContext.getReaderContext()
.error("One of 'strategy' and 'strategy-ref' must be set.",
frameElt);
}
}
else {
builder.addConstructorArgValue(header);
}
if (disabled) {
return;
}
}
if (addIfNotPresent || frameElt != null) {
headerWriters.add(builder.getBeanDefinition());
}
}
private void parseXssElement(boolean addIfNotPresent, Element element,
ParserContext parserContext) {
Element xssElt = element == null ? null : DomUtils.getChildElementByTagName(
element, XSS_ELEMENT);
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(XXssProtectionHeaderWriter.class);
if (xssElt != null) {
boolean disabled = "true".equals(getAttribute(xssElt, ATT_DISABLED, "false"));
String enabled = xssElt.getAttribute(ATT_ENABLED);
if (StringUtils.hasText(enabled)) {
if (disabled) {
attrNotAllowed(parserContext, ATT_ENABLED, ATT_DISABLED, xssElt);
}
builder.addPropertyValue("enabled", enabled);
}
String block = xssElt.getAttribute(ATT_BLOCK);
if (StringUtils.hasText(block)) {
if (disabled) {
attrNotAllowed(parserContext, ATT_BLOCK, ATT_DISABLED, xssElt);
}
builder.addPropertyValue("block", block);
}
if (disabled) {
return;
}
}
if (addIfNotPresent || xssElt != null) {
headerWriters.add(builder.getBeanDefinition());
}
}
private String getAttribute(Element element, String name, String defaultValue) {
if (element == null) {
return defaultValue;
}
String value = element.getAttribute(name);
if (StringUtils.hasText(value)) {
return value;
}
else {
return defaultValue;
}
}
}