/*
* $Id$
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.struts2.interceptor;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import com.opensymphony.xwork2.security.AcceptedPatternsChecker;
import com.opensymphony.xwork2.security.ExcludedPatternsChecker;
import com.opensymphony.xwork2.util.TextParseUtil;
import com.opensymphony.xwork2.util.ValueStack;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.ServletActionContext;
import javax.servlet.http.Cookie;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* <!-- START SNIPPET: description -->
*
* <p>
* The aim of this interceptor is to set values in the stack/action based on cookie name/value
* of interest.
* </p>
*
* <p>
* If an asterisk is present in cookiesName parameter, it will be assume that
* all cookies name are to be injected into struts' action, even though
* cookiesName is comma-separated by other values, e.g. (cookie1,*,cookie2).
* </p>
*
* <p>
* If cookiesName is left empty it will assume that no cookie will be injected
* into Struts' action.
* </p>
*
* <p>
* If an asterisk is present in cookiesValue parameter, it will assume that all
* cookies name irrespective of its value will be injected into Struts' action so
* long as the cookie name matches those specified in cookiesName parameter.
* </p>
*
* <p>
* If cookiesValue is left empty it will assume that all cookie that match the cookieName
* parameter will be injected into Struts' action.
* </p>
*
* <p>
* The action could implement {@link CookiesAware} in order to have a {@link Map}
* of filtered cookies set into it.
* </p>
*
* <!-- END SNIPPET: description -->
*
*
* <!-- START SNIPPET: parameters -->
*
* <ul>
* <li>cookiesName (mandatory) - Name of cookies to be injected into the action. If more
* than one cookie name is desired it could be comma-separated.
* If all cookies name is desired, it could simply be *, an asterik.
* When many cookies name are comma-separated either of the cookie
* that match the name in the comma-separated list will be qualified.</li>
* <li>cookiesValue (mandatory) - Value of cookies that if its name matches cookieName attribute
* and its value matched this, will be injected into Struts'
* action. If more than one cookie name is desired it could be
* comma-separated. If left empty, it will assume any value would
* be ok. If more than one value is specified (comma-separated)
* it will assume a match if either value is matched.</li>
* <li>acceptCookieNames (optional) - Pattern used to check if name of cookie matches the provided patter, to </li>
* </ul>
*
* <!-- END SNIPPET: parameters -->
*
*
* <!-- START SNIPPET: extending -->
*
* <ul>
* <li>
* populateCookieValueIntoStack - this method will decide if this cookie value is qualified
* to be populated into the value stack (hence into the action itself)
* </li>
* <li>
* injectIntoCookiesAwareAction - this method will inject selected cookies (as a java.util.Map)
* into action that implements {@link CookiesAware}.
* </li>
* </ul>
*
* <!-- END SNIPPET: extending -->
*
* <pre>
* <!-- START SNIPPET: example -->
*
* <!--
* This example will inject cookies named either 'cookie1' or 'cookie2' whose
* value could be either 'cookie1value' or 'cookie2value' into Struts' action.
* -->
* <action ... >
* <interceptor-ref name="cookie">
* <param name="cookiesName">cookie1, cookie2</param>
* <param name="cookiesValue">cookie1value, cookie2value</param>
* </interceptor-ref>
* ....
* </action>
*
*
* <!--
* This example will inject cookies named either 'cookie1' or 'cookie2'
* regardless of their value into Struts' action.
* -->
* <action ... >
* <interceptor-ref name="cookie">
* <param name="cookiesName">cookie1, cookie2</param>
* <param name="cookiesValue">*</param>
* <interceptor-ref>
* ...
* </action>
*
*
* <!--
* This example will inject cookies named either 'cookie1' with value
* 'cookie1value' or 'cookie2' with value 'cookie2value' into Struts'
* action.
* -->
* <action ... >
* <interceptor-ref name="cookie">
* <param name="cookiesName">cookie1</param>
* <param name="cookiesValue">cookie1value</param>
* </interceptor-ref>
* <interceptor-ref name="cookie">
* <param name="cookiesName"<cookie2</param>
* <param name="cookiesValue">cookie2value</param>
* </interceptor-ref>
* ....
* </action>
*
* <!--
* This example will inject any cookies regardless of its value into
* Struts' action.
* -->
* <action ... >
* <interceptor-ref name="cookie">
* <param name="cookiesName">*</param>
* <param name="cookiesValue">*</param>
* </interceptor-ref>
* ...
* </action>
*
* <!-- END SNIPPET: example -->
* </pre>
*
* @see CookiesAware
*/
public class CookieInterceptor extends AbstractInterceptor {
private static final long serialVersionUID = 4153142432948747305L;
private static final Logger LOG = LogManager.getLogger(CookieInterceptor.class);
private static final String ACCEPTED_PATTERN = "[a-zA-Z0-9\\.\\]\\[_'\\s]+";
private Set<String> cookiesNameSet = Collections.emptySet();
private Set<String> cookiesValueSet = Collections.emptySet();
private ExcludedPatternsChecker excludedPatternsChecker;
private AcceptedPatternsChecker acceptedPatternsChecker;
@Inject
public void setExcludedPatternsChecker(ExcludedPatternsChecker excludedPatternsChecker) {
this.excludedPatternsChecker = excludedPatternsChecker;
}
@Inject
public void setAcceptedPatternsChecker(AcceptedPatternsChecker acceptedPatternsChecker) {
this.acceptedPatternsChecker = acceptedPatternsChecker;
this.acceptedPatternsChecker.setAcceptedPatterns(ACCEPTED_PATTERN);
}
/**
* @param cookiesName the <code>cookiesName</code> which if matched will allow the cookie
* to be injected into action, could be comma-separated string.
*/
public void setCookiesName(String cookiesName) {
if (cookiesName != null) {
this.cookiesNameSet = TextParseUtil.commaDelimitedStringToSet(cookiesName);
}
}
/**
* @param cookiesValue the <code>cookiesValue</code> which if matched (together with matching
* cookiesName) will caused the cookie to be injected into action, could be
* comma-separated string.
*/
public void setCookiesValue(String cookiesValue) {
if (cookiesValue != null) {
this.cookiesValueSet = TextParseUtil.commaDelimitedStringToSet(cookiesValue);
}
}
/**
* Set the <code>acceptCookieNames</code> pattern of allowed names of cookies
* to protect against remote command execution vulnerability.
*
* @param commaDelimitedPattern is used to check cookie name against, can set of comma delimited patterns
*/
public void setAcceptCookieNames(String commaDelimitedPattern) {
acceptedPatternsChecker.setAcceptedPatterns(commaDelimitedPattern);
}
public String intercept(ActionInvocation invocation) throws Exception {
LOG.debug("start interception");
// contains selected cookies
final Map<String, String> cookiesMap = new LinkedHashMap<>();
Cookie[] cookies = ServletActionContext.getRequest().getCookies();
if (cookies != null) {
final ValueStack stack = ActionContext.getContext().getValueStack();
for (Cookie cookie : cookies) {
String name = cookie.getName();
String value = cookie.getValue();
if (isAcceptableName(name)) {
if (cookiesNameSet.contains("*")) {
LOG.debug("Contains cookie name [*] in configured cookies name set, cookie with name [{}] with value [{}] will be injected", name, value);
populateCookieValueIntoStack(name, value, cookiesMap, stack);
} else if (cookiesNameSet.contains(cookie.getName())) {
populateCookieValueIntoStack(name, value, cookiesMap, stack);
}
} else {
LOG.warn("Cookie name [{}] with value [{}] was rejected!", name, value);
}
}
}
// inject the cookiesMap, even if we don't have any cookies
injectIntoCookiesAwareAction(invocation.getAction(), cookiesMap);
return invocation.invoke();
}
/**
* Checks if name of Cookie doesn't contain vulnerable code
*
* @param name of Cookie
* @return true|false
*/
protected boolean isAcceptableName(String name) {
return !isExcluded(name) && isAccepted(name);
}
/**
* Checks if name/value of Cookie is acceptable
*
* @param name of Cookie
* @return true|false
*/
protected boolean isAccepted(String name) {
AcceptedPatternsChecker.IsAccepted accepted = acceptedPatternsChecker.isAccepted(name);
if (accepted.isAccepted()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Cookie [{}] matches acceptedPattern [{}]", name, accepted.getAcceptedPattern());
}
return true;
}
if (LOG.isTraceEnabled()) {
LOG.trace("Cookie [{}] doesn't match acceptedPattern [{}]", name, accepted.getAcceptedPattern());
}
return false;
}
/**
* Checks if name/value of Cookie is excluded
*
* @param name of Cookie
* @return true|false
*/
protected boolean isExcluded(String name) {
ExcludedPatternsChecker.IsExcluded excluded = excludedPatternsChecker.isExcluded(name);
if (excluded.isExcluded()) {
if (LOG.isTraceEnabled()) {
LOG.trace("Cookie [{}] matches excludedPattern [{}]", name, excluded.getExcludedPattern());
}
return true;
}
if (LOG.isTraceEnabled()) {
LOG.trace("Cookie [{}] doesn't match excludedPattern [{}]", name, excluded.getExcludedPattern());
}
return false;
}
/**
* Hook that populate cookie value into value stack (hence the action)
* if the criteria is satisfied (if the cookie value matches with those configured).
*
* @param cookieName cookie name
* @param cookieValue cookie value
* @param cookiesMap map of cookies
* @param stack value stack
*/
protected void populateCookieValueIntoStack(String cookieName, String cookieValue, Map<String, String> cookiesMap, ValueStack stack) {
if (cookiesValueSet.isEmpty() || cookiesValueSet.contains("*")) {
// If the interceptor is configured to accept any cookie value
// OR
// no cookiesValue is defined, so as long as the cookie name match
// we'll inject it into Struts' action
if (LOG.isDebugEnabled()) {
if (cookiesValueSet.isEmpty())
LOG.debug("no cookie value is configured, cookie with name [{}] with value [{}] will be injected", cookieName, cookieValue);
else if (cookiesValueSet.contains("*"))
LOG.debug("interceptor is configured to accept any value, cookie with name [{}] with value [{}] will be injected", cookieName, cookieValue);
}
cookiesMap.put(cookieName, cookieValue);
stack.setValue(cookieName, cookieValue);
}
else {
// if cookiesValues is specified, the cookie's value must match before we
// inject them into Struts' action
if (cookiesValueSet.contains(cookieValue)) {
LOG.debug("both configured cookie name and value matched, cookie [{}] with value [{}] will be injected", cookieName, cookieValue);
cookiesMap.put(cookieName, cookieValue);
stack.setValue(cookieName, cookieValue);
}
}
}
/**
* Hook that set the <code>cookiesMap</code> into action that implements
* {@link CookiesAware}.
*
* @param action action object
* @param cookiesMap map of cookies
*/
protected void injectIntoCookiesAwareAction(Object action, Map<String, String> cookiesMap) {
if (action instanceof CookiesAware) {
LOG.debug("Action [{}] implements CookiesAware, injecting cookies map [{}]", action, cookiesMap);
((CookiesAware)action).setCookiesMap(cookiesMap);
}
}
}