This is an automated email from the ASF dual-hosted git repository.
schultz pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push:
new 8a9f1be554 Csrf filter improvements (#681)
8a9f1be554 is described below
commit 8a9f1be5541e538fd120c243bd8f115b75516737
Author: Christopher Schultz <[email protected]>
AuthorDate: Thu Feb 1 10:02:27 2024 -0500
Csrf filter improvements (#681)
* Add an enforce() method and support for non-enforcement of CSRF
This allows subclasses to decide whether to enforce CSRF under whatever
conditions they choose.
* Add an "enforce" flag for CSRF prevention.
This allows developers to put the CSRF prevention filter into a monitoring
mode.
* Add no-nonce-URL patterns to suppress nonces for certain URLs
This improves cache performance for resources that need no protection.
* Whitespace police
* Add SVG to default list of no-nonce patterns.
* URLs that will not have nonces added to them should also be skipped for
enforcement.
* Re-organize constant members and re-factor a utility method.
* Simplify default no-nonce URL pattern definition.
* Add additional default no-nonce file extensions.
* Delay building of no-nonce predicates until after initialization
Capture servet context and make it available to predicate-construction.
* Introduce a MIME-type match for no-nonce URLs
* Add .jms file extension to default no-nonce list.
Align documentation with the actual default no-nonce list.
* Fix logic error.
* Optimize and fix logic error.
* Clarify documentation
* Consistency
* Use javabean semantics for boolean accessor
* Fix copy/paste logic error.
* Align documentation with javadoc.
* Make regular-expresson no-nonce patterns singletons.
There is no particular need to have multiple regular expressions, here.
* Fix broken unit test
* Fix obvious matching error with prefix and suffix predicates.
Restore regexp matching capability when parsing a single expression. This
allows regular expressions with MIME matching.
* Add unit tests.
* Add javadoc.
* Add changelog
---
.../catalina/filters/CsrfPreventionFilter.java | 392 +++++++++++++++++++--
.../catalina/filters/TestCsrfPreventionFilter.java | 110 +++++-
webapps/docs/changelog.xml | 4 +
webapps/docs/config/filter.xml | 42 +++
4 files changed, 508 insertions(+), 40 deletions(-)
diff --git a/java/org/apache/catalina/filters/CsrfPreventionFilter.java
b/java/org/apache/catalina/filters/CsrfPreventionFilter.java
index 2464ebc59c..b6b78dc7e3 100644
--- a/java/org/apache/catalina/filters/CsrfPreventionFilter.java
+++ b/java/org/apache/catalina/filters/CsrfPreventionFilter.java
@@ -18,15 +18,22 @@ package org.apache.catalina.filters;
import java.io.IOException;
import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
+import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -43,8 +50,38 @@ import org.apache.juli.logging.LogFactory;
* <li>{@link HttpServletResponse#encodeRedirectURL(String)} and {@link
HttpServletResponse#encodeURL(String)} are used
* to encode all URLs returned to the client
* </ul>
+ *
+ * <p>
+ * CSRF protection is enabled by generating random nonce values which are
+ * stored in the client's HTTP session. Each URL encoded using
+ * {@link HttpServletResponse#encodeURL(String)} has a URL parameter added
+ * which, when sent to the server in a future request, will be checked
+ * against this stored set of nonces for validity.
+ * </p>
+ *
+ * <p>
+ * Some URLs should be accessible even without a valid nonce parameter value.
+ * These URLs are known as "entry points" because clients should be able to
+ * "enter" the application without first establishing any valid tokens. These
+ * are configured with the <code>entryPoints</code> filter
+ * <code>init-param</code>.
+ * </p>
+ *
+ * <p>
+ * Some URLs should not have nonce parameters added to them at all
*/
public class CsrfPreventionFilter extends CsrfPreventionFilterBase {
+ /**
+ * The default set of URL patterns for which nonces will not be appended.
+ */
+ private static final String DEFAULT_NO_NONCE_URL_PATTERNS
+ = "*.css, *.js, *.gif, *.png, *.jpg, *.svg, *.ico, *.jpeg, *.mjs";
+
+ /**
+ * The servlet context in which this Filter is operating.
+ */
+ private ServletContext context;
+
private final Log log = LogFactory.getLog(CsrfPreventionFilter.class);
private final Set<String> entryPoints = new HashSet<>();
@@ -53,6 +90,20 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
private String nonceRequestParameterName =
Constants.CSRF_NONCE_REQUEST_PARAM;
+ /**
+ * Flag which determines whether this Filter is in "enforcement" mode
+ * (the default) or in "reporting" mode.
+ */
+ private boolean enforce = true;
+
+ /**
+ * A set of comma-separated URL patterns which will have no nonce
+ * parameters added to them.
+ */
+ private String noNoncePatterns = DEFAULT_NO_NONCE_URL_PATTERNS;
+
+ private Collection<Predicate<String>> noNoncePredicates;
+
/**
* Entry points are URLs that will not be tested for the presence of a
valid nonce. They are used to provide a way
* to navigate back to a protected application after navigating away from
it. Entry points will be limited to HTTP
@@ -87,11 +138,193 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
this.nonceRequestParameterName = parameterName;
}
+ /**
+ * Sets the flag to enforce CSRF protection or just log failures as DEBUG
+ * messages.
+ *
+ * @param enforce <code>true</code> to enforce CSRF protection or
+ * <code>false</code> to log DEBUG messages and allow
+ * all requests.
+ */
+ public void setEnforce(boolean enforce) {
+ this.enforce = enforce;
+ }
+
+ /**
+ * Gets the flag to enforce CSRF protection or just log failures as DEBUG
+ * messages.
+ *
+ * @return <code>true</code> if CSRF protection will be enforced or
+ * <code>false</code> if all requests will be allowed and
+ * failures will be logged as DEBUG messages.
+ */
+ public boolean isEnforce() {
+ return this.enforce;
+ }
+
+ /**
+ * Sets the list of URL patterns to suppress nonce-addition for.
+ *
+ * Some URLs do not need nonces added to them such as static resources.
+ * By <i>not</i> adding nonces to those URLs, HTTP caches can be more
+ * effective because the CSRF prevention filter won't generate what
+ * look like unique URLs for those commonly-reused resources.
+ *
+ * @param patterns A comma-separated list of URL patterns that will not
+ * have nonces added to them. Patterns may begin or end with a
+ * <code>*</code> character to denote a suffix-match or
+ * prefix-match. Any matched URL will not have a CSRF nonce
+ * added to it when passed through
+ * {@link HttpServletResponse#encodeURL(String)}.
+ */
+ public void setNoNonceURLPatterns(String patterns) {
+ this.noNoncePatterns = patterns;
+
+ if (null != context) {
+ this.noNoncePredicates = createNoNoncePredicates(context,
this.noNoncePatterns);
+ }
+ }
+
+ /**
+ * Creates a collection of matchers from a comma-separated string of
patterns.
+ *
+ * @param patterns A comma-separated string of URL matching patterns.
+ *
+ * @return A collection of predicates representing the URL patterns.
+ */
+ protected static Collection<Predicate<String>>
createNoNoncePredicates(ServletContext context, String patterns) {
+ if (null == patterns || 0 == patterns.trim().length()) {
+ return null;
+ }
+
+ if (patterns.startsWith("/") && patterns.endsWith("/")) {
+ return Collections.singleton(new
PatternPredicate(patterns.substring(1, patterns.length() - 1)));
+ }
+
+ String values[] = patterns.split(",");
+
+ ArrayList<Predicate<String>> matchers = new ArrayList<>(values.length);
+ for (String value : values) {
+ Predicate<String> p = createNoNoncePredicate(context,
value.trim());
+
+ if (null != p) {
+ matchers.add(p);
+ }
+ }
+
+ matchers.trimToSize();
+
+ return matchers;
+ }
+
+ /**
+ * Creates a predicate that can match the specified type of pattern.
+ *
+ * @param pattern The pattern to match e.g. <code>*.foo</code> or
+ * <code>/bar/*</code>.
+ *
+ * @return A Predicate which can match the specified pattern, or
+ * <code>>null</code> if the pattern is null or blank.
+ */
+ protected static Predicate<String> createNoNoncePredicate(ServletContext
context, String pattern) {
+ if (null == pattern || 0 == pattern.trim().length()) {
+ return null;
+ }
+ if (pattern.startsWith("mime:")) {
+ return new MimePredicate(context, createNoNoncePredicate(context,
pattern.substring(5)));
+ } else if (pattern.startsWith("*")) {
+ return new SuffixPredicate(pattern.substring(1));
+ } else if (pattern.endsWith("*")) {
+ return new PrefixPredicate(pattern.substring(0, pattern.length() -
1));
+ } else if (pattern.startsWith("/") && pattern.endsWith("/")) {
+ return new PatternPredicate(pattern.substring(1, pattern.length()
- 1));
+ } else {
+ throw new IllegalArgumentException("Unsupported pattern: " +
pattern);
+ }
+ }
+
+ /**
+ * A no-nonce Predicate that evaluates a MIME type instead of a URL.
+ *
+ * It can be used with any other Predicate for matching
+ * the actual value of the MIME type.
+ */
+ protected static class MimePredicate implements Predicate<String> {
+ private final ServletContext context;
+ private final Predicate<String> predicate;
+
+ public MimePredicate(ServletContext context, Predicate<String>
predicate) {
+ this.context = context;
+ this.predicate = predicate;
+ }
+
+ @Override
+ public boolean test(String t) {
+ String mimeType = context.getMimeType(t);
+
+ return predicate.test(mimeType);
+ }
+
+ public Predicate<String> getPredicate() {
+ return predicate;
+ }
+ }
+
+ /**
+ * A no-nonce Predicate that matches a prefix.
+ */
+ protected static class PrefixPredicate implements Predicate<String> {
+ private final String prefix;
+ public PrefixPredicate(String prefix) {
+ this.prefix = prefix;
+ }
+
+ @Override
+ public boolean test(String t) {
+ return t.startsWith(this.prefix);
+ }
+ }
+
+ /**
+ * A no-nonce Predicate that matches a suffix.
+ */
+ protected static class SuffixPredicate implements Predicate<String> {
+ private final String suffix;
+ public SuffixPredicate(String suffix) {
+ this.suffix = suffix;
+ }
+
+ @Override
+ public boolean test(String t) {
+ return t.endsWith(this.suffix);
+ }
+ }
+
+ /**
+ * A no-nonce Predicate that matches a regular expression.
+ */
+ protected static class PatternPredicate implements Predicate<String> {
+ private final Pattern pattern;
+
+ public PatternPredicate(String regex) {
+ this.pattern = Pattern.compile(regex);
+ }
+
+ @Override
+ public boolean test(String t) {
+ return pattern.matcher(t).matches();
+ }
+ }
+
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// Set the parameters
super.init(filterConfig);
+ this.context = filterConfig.getServletContext();
+
+ this.noNoncePredicates = createNoNoncePredicates(context,
this.noNoncePatterns);
+
// Put the expected request parameter name into the application scope
filterConfig.getServletContext().setAttribute(Constants.CSRF_NONCE_REQUEST_PARAM_NAME_KEY,
nonceRequestParameterName);
@@ -100,7 +333,6 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
-
ServletResponse wResponse = null;
if (request instanceof HttpServletRequest && response instanceof
HttpServletResponse) {
@@ -110,6 +342,7 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
HttpSession session = req.getSession(false);
+ String requestedPath = getRequestedPath(req);
boolean skipNonceCheck = skipNonceCheck(req);
NonceCache<String> nonceCache = null;
@@ -117,38 +350,62 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
String previousNonce =
req.getParameter(nonceRequestParameterName);
if (previousNonce == null) {
- if (log.isDebugEnabled()) {
- log.debug("Rejecting request for " +
getRequestedPath(req) + ", session " +
- (null == session ? "(none)" : session.getId())
+
- " with no CSRF nonce found in request");
- }
-
- res.sendError(getDenyStatus());
- return;
- }
+ if (enforce(req, requestedPath)) {
+ if (log.isDebugEnabled()) {
+ log.debug("Rejecting request for " +
getRequestedPath(req) + ", session " +
+ (null == session ? "(none)" :
session.getId()) +
+ " with no CSRF nonce found in request");
+ }
- nonceCache = getNonceCache(req, session);
- if (nonceCache == null) {
- if (log.isDebugEnabled()) {
- log.debug("Rejecting request for " +
getRequestedPath(req) + ", session " +
- (null == session ? "(none)" : session.getId())
+ " due to empty / missing nonce cache");
+ res.sendError(getDenyStatus());
+ return;
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug("Would have rejected request for " +
getRequestedPath(req) + ", session " +
+ (null == session ? "(none)" :
session.getId()) +
+ " with no CSRF nonce found in request");
+ }
}
-
- res.sendError(getDenyStatus());
- return;
- } else if (!nonceCache.contains(previousNonce)) {
- if (log.isDebugEnabled()) {
- log.debug("Rejecting request for " +
getRequestedPath(req) + ", session " +
- (null == session ? "(none)" : session.getId())
+ " due to invalid nonce " +
- previousNonce);
+ } else {
+ nonceCache = getNonceCache(req, session);
+ if (nonceCache == null) {
+ if (enforce(req, requestedPath)) {
+ if (log.isDebugEnabled()) {
+ log.debug("Rejecting request for " +
getRequestedPath(req) + ", session " +
+ (null == session ? "(none)" :
session.getId()) + " due to empty / missing nonce cache");
+ }
+
+ res.sendError(getDenyStatus());
+ return;
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug("Would have rejecting request for "
+ getRequestedPath(req) + ", session " +
+ (null == session ? "(none)" :
session.getId()) + " due to empty / missing nonce cache");
+ }
+ }
+ } else if (!nonceCache.contains(previousNonce)) {
+ if (enforce(req, requestedPath)) {
+ if (log.isDebugEnabled()) {
+ log.debug("Rejecting request for " +
getRequestedPath(req) + ", session " +
+ (null == session ? "(none)" :
session.getId()) + " due to invalid nonce " +
+ previousNonce);
+ }
+
+ res.sendError(getDenyStatus());
+ return;
+ } else {
+ if (log.isDebugEnabled()) {
+ log.debug("Would have rejecting request for "
+ getRequestedPath(req) + ", session " +
+ (null == session ? "(none)" :
session.getId()) + " due to invalid nonce " +
+ previousNonce);
+ }
+ }
+ } else {
+ if (log.isTraceEnabled()) {
+ log.trace(
+ "Allowing request to " +
getRequestedPath(req) + " with valid CSRF nonce " + previousNonce);
+ }
}
-
- res.sendError(getDenyStatus());
- return;
- }
- if (log.isTraceEnabled()) {
- log.trace(
- "Allowing request to " + getRequestedPath(req) + "
with valid CSRF nonce " + previousNonce);
}
}
@@ -183,13 +440,31 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
// requiring the use of response.encodeURL.
request.setAttribute(Constants.CSRF_NONCE_REQUEST_ATTR_NAME,
newNonce);
- wResponse = new CsrfResponseWrapper(res,
nonceRequestParameterName, newNonce);
+ wResponse = new CsrfResponseWrapper(res,
nonceRequestParameterName, newNonce, noNoncePredicates);
}
}
chain.doFilter(request, wResponse == null ? response : wResponse);
}
+ /**
+ * Check to see if the request and path should be enforced or only
+ * observed and reported.
+ *
+ * Note that the <code>requestedPath</code> parameter is purely
+ * a performance optimization to avoid calling
+ * {@link #getRequestedPath(HttpServletRequest)} multiple times.
+ *
+ * @param req The request.
+ * @param requestedPath The path of the request being evaluated.
+ *
+ * @return <code>true</code> if the CSRF prevention should be enforced,
+ * <code>false</code> if the CSRF prevention should only be
+ * logged in DEBUG mode.
+ */
+ protected boolean enforce(HttpServletRequest req, String requestedPath) {
+ return isEnforce();
+ }
protected boolean skipNonceCheck(HttpServletRequest request) {
if (!Constants.METHOD_GET.equals(request.getMethod())) {
@@ -198,15 +473,27 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
String requestedPath = getRequestedPath(request);
- if (!entryPoints.contains(requestedPath)) {
- return false;
+ if (entryPoints.contains(requestedPath)) {
+ if (log.isTraceEnabled()) {
+ log.trace("Skipping CSRF nonce-check for GET request to entry
point " + requestedPath);
+ }
+
+ return true;
}
- if (log.isTraceEnabled()) {
- log.trace("Skipping CSRF nonce-check for GET request to entry
point " + requestedPath);
+ if (null != noNoncePredicates && !noNoncePredicates.isEmpty()) {
+ for (Predicate<String> p : noNoncePredicates) {
+ if (p.test(requestedPath)) {
+ if (log.isTraceEnabled()) {
+ log.trace("Skipping CSRF nonce-check for GET request
to no-nonce path " + requestedPath);
+ }
+
+ return true;
+ }
+ }
}
- return true;
+ return false;
}
@@ -267,11 +554,14 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
private final String nonceRequestParameterName;
private final String nonce;
+ private final Collection<Predicate<String>> noNoncePatterns;
- public CsrfResponseWrapper(HttpServletResponse response, String
nonceRequestParameterName, String nonce) {
+ public CsrfResponseWrapper(HttpServletResponse response, String
nonceRequestParameterName,
+ String nonce, Collection<Predicate<String>> noNoncePatterns) {
super(response);
this.nonceRequestParameterName = nonceRequestParameterName;
this.nonce = nonce;
+ this.noNoncePatterns = noNoncePatterns;
}
@Override
@@ -282,7 +572,11 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
@Override
public String encodeRedirectURL(String url) {
- return addNonce(super.encodeRedirectURL(url));
+ if (shouldAddNonce(url)) {
+ return addNonce(super.encodeRedirectURL(url));
+ } else {
+ return url;
+ }
}
@Override
@@ -293,7 +587,27 @@ public class CsrfPreventionFilter extends
CsrfPreventionFilterBase {
@Override
public String encodeURL(String url) {
- return addNonce(super.encodeURL(url));
+ if (shouldAddNonce(url)) {
+ return addNonce(super.encodeURL(url));
+ } else {
+ return url;
+ }
+ }
+
+ private boolean shouldAddNonce(String url) {
+ if (null == noNoncePatterns || noNoncePatterns.isEmpty()) {
+ return true;
+ }
+
+ if (null != noNoncePatterns) {
+ for (Predicate<String> p : noNoncePatterns) {
+ if (p.test(url)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
}
/*
diff --git a/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java
b/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java
index 603db99d02..b9e0c06f3c 100644
--- a/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java
+++ b/test/org/apache/catalina/filters/TestCsrfPreventionFilter.java
@@ -20,6 +20,11 @@ import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.function.Predicate;
import javax.servlet.http.HttpServletResponse;
@@ -28,13 +33,14 @@ import org.junit.Test;
import org.apache.catalina.filters.CsrfPreventionFilter.LruCache;
import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.unittest.TesterServletContext;
public class TestCsrfPreventionFilter extends TomcatBaseTest {
private static final String RESULT_NONCE =
Constants.CSRF_NONCE_SESSION_ATTR_NAME + "=TESTNONCE";
private final HttpServletResponse wrapper = new
CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(),
- Constants.CSRF_NONCE_SESSION_ATTR_NAME, "TESTNONCE");
+ Constants.CSRF_NONCE_SESSION_ATTR_NAME, "TESTNONCE", null);
@Test
public void testAddNonceNoQueryNoAnchor() throws Exception {
@@ -92,6 +98,108 @@ public class TestCsrfPreventionFilter extends
TomcatBaseTest {
}
}
+
+ @Test
+ public void testNoNonceBuilders() {
+ Assert.assertEquals(CsrfPreventionFilter.PrefixPredicate.class,
CsrfPreventionFilter.createNoNoncePredicate(null, "/images/*").getClass());
+ Assert.assertEquals(CsrfPreventionFilter.SuffixPredicate.class,
CsrfPreventionFilter.createNoNoncePredicate(null, "*.png").getClass());
+ Assert.assertEquals(CsrfPreventionFilter.PatternPredicate.class,
CsrfPreventionFilter.createNoNoncePredicate(null,
"/^(/images/.*|.*\\.png)$/").getClass());
+
+ Collection<Predicate<String>> chain =
CsrfPreventionFilter.createNoNoncePredicates(null,
"*.png,/js/*,*.jpg,/images/*,mime:*/png,mime:image/*");
+
+ Assert.assertEquals(6, chain.size());
+ Iterator<Predicate<String>> items = chain.iterator();
+
+ Assert.assertEquals(CsrfPreventionFilter.SuffixPredicate.class,
items.next().getClass());
+ Assert.assertEquals(CsrfPreventionFilter.PrefixPredicate.class,
items.next().getClass());
+ Assert.assertEquals(CsrfPreventionFilter.SuffixPredicate.class,
items.next().getClass());
+ Assert.assertEquals(CsrfPreventionFilter.PrefixPredicate.class,
items.next().getClass());
+ Predicate<String> item = items.next();
+ Assert.assertEquals(CsrfPreventionFilter.MimePredicate.class,
item.getClass());
+ Assert.assertEquals(CsrfPreventionFilter.SuffixPredicate.class,
((CsrfPreventionFilter.MimePredicate)item).getPredicate().getClass());
+
+ item = items.next();
+ Assert.assertEquals(CsrfPreventionFilter.MimePredicate.class,
item.getClass());
+ Assert.assertEquals(CsrfPreventionFilter.PrefixPredicate.class,
((CsrfPreventionFilter.MimePredicate)item).getPredicate().getClass());
+ }
+
+ @Test
+ public void testNoNoncePatternMatchers() {
+ String[] urls = { "/images/home.png" };
+ Predicate<String> prefix = new
CsrfPreventionFilter.PrefixPredicate("/images/");
+ Predicate<String> suffix = new
CsrfPreventionFilter.SuffixPredicate(".png");
+ Predicate<String> regex = new
CsrfPreventionFilter.PatternPredicate("^(/images/.*|.*\\.png)$");
+
+ for(String url : urls) {
+ Assert.assertTrue("Prefix match fails", prefix.test(url));
+ Assert.assertTrue("Suffix match fails", suffix.test(url));
+ Assert.assertTrue("Pattern match fails", regex.test(url));
+ }
+
+ ArrayList<Predicate<String>> chain = new ArrayList<>();
+ chain.add(prefix);
+ chain.add(suffix);
+ chain.add(regex);
+
+ HttpServletResponse response = new
CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(),
+ Constants.CSRF_NONCE_SESSION_ATTR_NAME, "TESTNONCE", chain);
+
+ // These URLs should include nonces
+ Assert.assertEquals("/foo?" + RESULT_NONCE,
response.encodeURL("/foo"));
+ Assert.assertEquals("/foo/images?" + RESULT_NONCE,
response.encodeURL("/foo/images"));
+ Assert.assertEquals("/foo/images/home.jpg?" + RESULT_NONCE,
response.encodeURL("/foo/images/home.jpg"));
+
+ // These URLs should not
+ Assert.assertEquals("/images/home.png",
response.encodeURL("/images/home.png"));
+ Assert.assertEquals("/images/home.jpg",
response.encodeURL("/images/home.jpg"));
+ Assert.assertEquals("/home.png", response.encodeURL("/home.png"));
+ Assert.assertEquals("/home.png", response.encodeURL("/home.png"));
+ }
+
+ @Test
+ public void testNoNonceMimeMatcher() {
+ MimeTypeServletContext context = new MimeTypeServletContext();
+ Predicate<String> mime = new
CsrfPreventionFilter.MimePredicate(context, new
CsrfPreventionFilter.PrefixPredicate("image/"));
+
+ context.setMimeType("image/png");
+ Assert.assertTrue("MIME match fails", mime.test("/images/home.png"));
+
+ context.setMimeType("text/plain");
+ Assert.assertFalse("MIME match succeeds where it should fail",
mime.test("/test.txt"));
+
+ Collection<Predicate<String>> chain = Collections.singleton(mime);
+ HttpServletResponse response = new
CsrfPreventionFilter.CsrfResponseWrapper(new NonEncodingResponse(),
+ Constants.CSRF_NONCE_SESSION_ATTR_NAME, "TESTNONCE", chain);
+
+ // These URLs should include nonces
+ Assert.assertEquals("/foo?" + RESULT_NONCE,
response.encodeURL("/foo"));
+ Assert.assertEquals("/foo/images?" + RESULT_NONCE,
response.encodeURL("/foo/images"));
+ Assert.assertEquals("/foo/images/home.jpg?" + RESULT_NONCE,
response.encodeURL("/foo/images/home.jpg"));
+ Assert.assertEquals("/images/home.png?" + RESULT_NONCE,
response.encodeURL("/images/home.png"));
+ Assert.assertEquals("/images/home.jpg?" + RESULT_NONCE,
response.encodeURL("/images/home.jpg"));
+ Assert.assertEquals("/home.png?" + RESULT_NONCE,
response.encodeURL("/home.png"));
+
+ context.setMimeType("image/png");
+ // These URLs should not
+ Assert.assertEquals("/images/home.png",
response.encodeURL("/images/home.png"));
+ Assert.assertEquals("/images/home.jpg",
response.encodeURL("/images/home.jpg"));
+ Assert.assertEquals("/home.png", response.encodeURL("/home.png"));
+ Assert.assertEquals("/foo", response.encodeURL("/foo"));
+ Assert.assertEquals("/foo/home.png",
response.encodeURL("/foo/home.png"));
+ Assert.assertEquals("/foo/images/home.jpg",
response.encodeURL("/foo/images/home.jpg"));
+ }
+
+ private static class MimeTypeServletContext extends TesterServletContext {
+ private String mimeType;
+ public void setMimeType(String type) {
+ mimeType = type;
+ }
+
+ @Override
+ public String getMimeType(String url) {
+ return mimeType;
+ }
+ }
private static class NonEncodingResponse extends TesterHttpServletResponse
{
@Override
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 8f1b0c6652..9530ce2c1b 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -118,6 +118,10 @@
sequences are correctly removed from files containing property values
when configured to do so. Bug identified by Coverity Scan. (markt)
</fix>
+ <add>
+ Add improvements to the CSRF prevention filter including the ability
+ to skip adding nonces for resource name and subtree URL patterns.
(schultz)
+ </add>
</changelog>
</subsection>
<subsection name="Coyote">
diff --git a/webapps/docs/config/filter.xml b/webapps/docs/config/filter.xml
index 053f0277a8..12e70181de 100644
--- a/webapps/docs/config/filter.xml
+++ b/webapps/docs/config/filter.xml
@@ -291,6 +291,13 @@
request. The default value is <code>403</code>.</p>
</attribute>
+ <attribute name="enforce" required="false">
+ <p>A flag to enable or disable enforcement. When enforcement is
+ disabled, the CsrfPreventionFilter will <i>allow all requests</i> and
+ log CSRF failures as DEBUG messages. The default is <b>true</b>,
+ enabling the enforcement of CSRF protection.</p>
+ </attribute>
+
<attribute name="entryPoints" required="false">
<p>A comma separated list of URLs that will not be tested for the
presence of a valid nonce. They are used to provide a way to navigate
@@ -319,6 +326,41 @@
of <code>java.security.SecureRandom</code> will be used.</p>
</attribute>
+ <attribute name="noNonceURLPatterns" required="false">
+ <p>A list of URL patterns that will <i>not</i> have CSRF nonces added
+ to them. You may not want to add nonces to certain URLs to avoid
+ creating unique URLs which may defeat resource caching, etc.</p>
+
+ <p>There are several types of patterns supported:</p>
+
+ <ul>
+ <li>Prefix matches using a pattern that ends with a <code>*</code>.
+ For example, <code>/images/*</code>.</li>
+
+ <li>Suffix matches using a pattern that begins with a <code>*</code>.
+ For example, <code>*.css</code>.</li>
+
+ <li>Mime-type matches which begin with <code>mime:</code> and specify
+ one of the above matches which will be checked against the MIME type
+ of the URL filename. For example, <code>mime:image/*</code>.
+ Note that the MIME-type will be determined using
+ <code>ServletContext.getMimeType</code>.</li>
+
+ <li>A single complete regular expression pattern which begins and
+ ends with <code>/</code> (slash / solidus) symbols. For example
+ <code>//images/.*|/scripts/.*/</code>. The leading and trailing
+ <code>/</code> characters will be removed from the pattern before
+ being compiled. Note that there can be only a single pattern,
+ but that pattern can of course have as many alternatives as desired
+ by using the regular expression <code>|</code> (<code>OR</code>)
+ operator. The regular expression will be matched against the entire
+ URL (i.e. <i>match</i> not <i>find</i> semantics), and the regex
+ dialect is Java (<code>java.util.regex.Pattern</code>).
+ </li>
+ </ul>
+
+ <p>The default is <code>*.css, *.js, *.gif, *.png, *.jpg, *.svg,
*.ico, *.jpeg, *.mjs</code>.</p>
+ </attribute>
</attributes>
</subsection>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]