All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.catalina.filters.CsrfPreventionFilter Maven / Gradle / Ivy

There is a newer version: 11.0.0-M24
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.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 jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import jakarta.servlet.http.HttpSession;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

/**
 * Provides basic CSRF protection for a web application. The filter assumes that:
 * 
    *
  • The filter is mapped to /*
  • *
  • {@link HttpServletResponse#encodeRedirectURL(String)} and {@link HttpServletResponse#encodeURL(String)} are used * to encode all URLs returned to the client *
*

* 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. *

*

* 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 entryPoints filter init-param. *

*

* 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 entryPoints = new HashSet<>(); private int nonceCacheSize = 5; 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> 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 * GET requests and should not trigger any security sensitive actions. * * @param entryPoints Comma separated list of URLs to be configured as entry points. */ public void setEntryPoints(String entryPoints) { String values[] = entryPoints.split(","); for (String value : values) { this.entryPoints.add(value.trim()); } } /** * Sets the number of previously issued nonces that will be cached on a LRU basis to support parallel requests, * limited use of the refresh and back in the browser and similar behaviors that may result in the submission of a * previous nonce rather than the current one. If not set, the default value of 5 will be used. * * @param nonceCacheSize The number of nonces to cache */ public void setNonceCacheSize(int nonceCacheSize) { this.nonceCacheSize = nonceCacheSize; } /** * Sets the request parameter name to use for CSRF nonces. * * @param parameterName The request parameter name to use for CSRF nonces. */ public void setNonceRequestParameterName(String parameterName) { this.nonceRequestParameterName = parameterName; } /** * Sets the flag to enforce CSRF protection or just log failures as DEBUG messages. * * @param enforce true to enforce CSRF protection or false 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 true if CSRF protection will be enforced or false 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 not 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 * 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 context the Servlet context * @param patterns A comma-separated string of URL matching patterns. * * @return A collection of predicates representing the URL patterns. */ protected static Collection> 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> matchers = new ArrayList<>(values.length); for (String value : values) { Predicate 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 context the Servlet context * @param pattern The pattern to match e.g. *.foo or /bar/*. * * @return A Predicate which can match the specified pattern, or null if the pattern is null or blank. */ protected static Predicate 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(sm.getString("csrfPrevention.unsupportedPattern", 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 { private final ServletContext context; private final Predicate predicate; public MimePredicate(ServletContext context, Predicate predicate) { this.context = context; this.predicate = predicate; } @Override public boolean test(String t) { String mimeType = context.getMimeType(t); return predicate.test(mimeType); } public Predicate getPredicate() { return predicate; } } /** * A no-nonce Predicate that matches a prefix. */ protected static class PrefixPredicate implements Predicate { 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 { 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 { 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); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletResponse wResponse = null; if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse res = (HttpServletResponse) response; HttpSession session = req.getSession(false); String requestedPath = getRequestedPath(req); boolean skipNonceCheck = skipNonceCheck(req); NonceCache nonceCache = null; if (!skipNonceCheck) { String previousNonce = req.getParameter(nonceRequestParameterName); if (previousNonce == null) { if (enforce(req, requestedPath)) { if (log.isDebugEnabled()) { log.debug(sm.getString("csrfPrevention.rejectNoNonce", getRequestedPath(req), (null == session ? "(null)" : session.getId()))); } res.sendError(getDenyStatus()); return; } else { if (log.isTraceEnabled()) { log.trace("Would have rejected request for " + getRequestedPath(req) + ", session " + (null == session ? "(null)" : session.getId()) + " with no CSRF nonce found in request"); } } } else { nonceCache = getNonceCache(req, session); if (nonceCache == null) { if (enforce(req, requestedPath)) { if (log.isDebugEnabled()) { log.debug(sm.getString("csrfPrevention.rejectNoCache", getRequestedPath(req), (null == session ? "(null)" : session.getId()))); } res.sendError(getDenyStatus()); return; } else { if (log.isTraceEnabled()) { log.trace("Would have rejecting request for " + getRequestedPath(req) + ", session " + (null == session ? "(null)" : session.getId()) + " due to empty / missing nonce cache"); } } } else if (!nonceCache.contains(previousNonce)) { if (enforce(req, requestedPath)) { if (log.isDebugEnabled()) { log.debug(sm.getString("csrfPrevention.rejectInvalidNonce", getRequestedPath(req), (null == session ? "(null)" : session.getId()), previousNonce)); } res.sendError(getDenyStatus()); return; } else { if (log.isTraceEnabled()) { log.trace("Would have rejecting request for " + getRequestedPath(req) + ", session " + (null == session ? "(null)" : session.getId()) + " due to invalid nonce " + previousNonce); } } } else { if (log.isTraceEnabled()) { log.trace("Allowing request to " + getRequestedPath(req) + " with valid CSRF nonce " + previousNonce); } } } } if (!skipNonceGeneration(req)) { if (skipNonceCheck) { // Didn't look up nonce cache earlier so look it up now. nonceCache = getNonceCache(req, session); } if (nonceCache == null) { if (log.isDebugEnabled()) { log.debug(sm.getString("csrfPrevention.createCache", Integer.valueOf(nonceCacheSize), (null == session ? "(null)" : session.getId()))); } if (session == null) { if (log.isTraceEnabled()) { log.trace("Creating new session to store CSRF nonce cache"); } session = req.getSession(true); } nonceCache = createNonceCache(req, session); } String newNonce = generateNonce(req); nonceCache.add(newNonce); // Take this request's nonce and put it into the request // attributes so pages can make direct use of it, rather than // requiring the use of response.encodeURL. request.setAttribute(Constants.CSRF_NONCE_REQUEST_ATTR_NAME, 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 * requestedPath 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 true if the CSRF prevention should be enforced, false 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())) { return false; } String requestedPath = getRequestedPath(request); if (entryPoints.contains(requestedPath)) { if (log.isTraceEnabled()) { log.trace("Skipping CSRF nonce-check for GET request to entry point " + requestedPath); } return true; } if (null != noNoncePredicates && !noNoncePredicates.isEmpty()) { for (Predicate 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 false; } /** * Determines whether a nonce should be created. This method is provided primarily for the benefit of sub-classes * that wish to customise this behaviour. * * @param request The request that triggered the need to potentially create the nonce. * * @return {@code true} if a nonce should be created, otherwise {@code false} */ protected boolean skipNonceGeneration(HttpServletRequest request) { return false; } /** * Create a new {@link NonceCache} and store in the {@link HttpSession}. This method is provided primarily for the * benefit of sub-classes that wish to customise this behaviour. * * @param request The request that triggered the need to create the nonce cache. Unused by the default * implementation. * @param session The session associated with the request. * * @return A newly created {@link NonceCache} */ protected NonceCache createNonceCache(HttpServletRequest request, HttpSession session) { NonceCache nonceCache = new LruCache<>(nonceCacheSize); session.setAttribute(Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonceCache); return nonceCache; } /** * Obtain the {@link NonceCache} associated with the request and/or session. This method is provided primarily for * the benefit of sub-classes that wish to customise this behaviour. * * @param request The request that triggered the need to obtain the nonce cache. Unused by the default * implementation. * @param session The session associated with the request. * * @return The {@link NonceCache} currently associated with the request and/or session */ protected NonceCache getNonceCache(HttpServletRequest request, HttpSession session) { if (session == null) { return null; } @SuppressWarnings("unchecked") NonceCache nonceCache = (NonceCache) session.getAttribute(Constants.CSRF_NONCE_SESSION_ATTR_NAME); return nonceCache; } protected static class CsrfResponseWrapper extends HttpServletResponseWrapper { private final String nonceRequestParameterName; private final String nonce; private final Collection> noNoncePatterns; public CsrfResponseWrapper(HttpServletResponse response, String nonceRequestParameterName, String nonce, Collection> noNoncePatterns) { super(response); this.nonceRequestParameterName = nonceRequestParameterName; this.nonce = nonce; this.noNoncePatterns = noNoncePatterns; } @Override public String encodeRedirectURL(String url) { if (shouldAddNonce(url)) { return addNonce(super.encodeRedirectURL(url)); } else { return url; } } @Override public String encodeURL(String 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 p : noNoncePatterns) { if (p.test(url)) { return false; } } } return true; } /* * Return the specified URL with the nonce added to the query string. * * @param url URL to be modified */ private String addNonce(String url) { if (url == null || nonce == null) { return url; } String path = url; String query = ""; String anchor = ""; int pound = path.indexOf('#'); if (pound >= 0) { anchor = path.substring(pound); path = path.substring(0, pound); } int question = path.indexOf('?'); if (question >= 0) { query = path.substring(question); path = path.substring(0, question); } StringBuilder sb = new StringBuilder(path); if (query.length() > 0) { sb.append(query); sb.append('&'); } else { sb.append('?'); } sb.append(nonceRequestParameterName); sb.append('='); sb.append(nonce); sb.append(anchor); return sb.toString(); } } protected interface NonceCache extends Serializable { void add(T nonce); boolean contains(T nonce); } /** * Despite its name, this is a FIFO cache not an LRU cache. Using an older nonce should not delay its removal from * the cache in favour of more recent values. * * @param The type held by this cache. */ protected static class LruCache implements NonceCache { private static final long serialVersionUID = 1L; // Although the internal implementation uses a Map, this cache // implementation is only concerned with the keys. private final Map cache; public LruCache(final int cacheSize) { cache = new LinkedHashMap<>() { private static final long serialVersionUID = 1L; @Override protected boolean removeEldestEntry(Map.Entry eldest) { if (size() > cacheSize) { return true; } return false; } }; } @Override public void add(T key) { synchronized (cache) { cache.put(key, null); } } @Override public boolean contains(T key) { synchronized (cache) { return cache.containsKey(key); } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy