This is an automated email from the ASF dual-hosted git repository. schultz pushed a commit to branch 8.5.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 7fb4beb75dfbcd7b667548e12068e2a649c487f3 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 * Use locally-defined Predicate interface for Java 7-based builds. --- .../catalina/filters/CsrfPreventionFilter.java | 395 +++++++++++++++++++-- .../catalina/filters/TestCsrfPreventionFilter.java | 110 +++++- webapps/docs/changelog.xml | 4 + webapps/docs/config/filter.xml | 42 +++ 4 files changed, 511 insertions(+), 40 deletions(-) diff --git a/java/org/apache/catalina/filters/CsrfPreventionFilter.java b/java/org/apache/catalina/filters/CsrfPreventionFilter.java index 2464ebc59c..a0d0369532 100644 --- a/java/org/apache/catalina/filters/CsrfPreventionFilter.java +++ b/java/org/apache/catalina/filters/CsrfPreventionFilter.java @@ -18,13 +18,18 @@ 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.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.ServletResponse; @@ -43,8 +48,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 +88,24 @@ 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 interface Predicate<T> { + boolean test(T t); + } + + 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 +140,194 @@ 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("/")) { + // NOTE: Java 7 requires this cast. + return Collections.singleton((Predicate<String>)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 +336,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 +345,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 +353,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 +443,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 +476,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 +557,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 +575,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 +590,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 0d671a80ea..e44b6c2d2d 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -112,6 +112,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