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 <ch...@christopherschultz.net>
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: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to