blob: b4fb4bb5a9a3c7d9bd38181a94b4323bcd968847 [file] [log] [blame]
/*
* 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.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/**
* Provides basic CSRF protection for REST APIs. The filter assumes that the
* clients have adapted the transfer of the nonce through the 'X-CSRF-Token'
* header.
*
* <pre>
* Positive scenario:
* Client Server
* | |
* | GET Fetch Request \| JSESSIONID
* |---------------------------------| X-CSRF-Token
* | /| pair generation
* |/Response to Fetch Request |
* |---------------------------------|
* JSESSIONID |\ |
* X-CSRF-Token | |
* pair cached | POST Request with valid nonce \| JSESSIONID
* |---------------------------------| X-CSRF-Token
* | /| pair validation
* |/ Response to POST Request |
* |---------------------------------|
* |\ |
*
* Negative scenario:
* Client Server
* | |
* | POST Request without nonce \| JSESSIONID
* |---------------------------------| X-CSRF-Token
* | /| pair validation
* |/Request is rejected |
* |---------------------------------|
* |\ |
*
* Client Server
* | |
* | POST Request with invalid nonce\| JSESSIONID
* |---------------------------------| X-CSRF-Token
* | /| pair validation
* |/Request is rejected |
* |---------------------------------|
* |\ |
* </pre>
*/
public class RestCsrfPreventionFilter extends CsrfPreventionFilterBase {
private enum MethodType {
NON_MODIFYING_METHOD, MODIFYING_METHOD
}
private static final Pattern NON_MODIFYING_METHODS_PATTERN = Pattern
.compile("GET|HEAD|OPTIONS");
private Set<String> pathsAcceptingParams = new HashSet<>();
private String pathsDelimiter = ",";
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
MethodType mType = MethodType.MODIFYING_METHOD;
String method = ((HttpServletRequest) request).getMethod();
if (method != null && NON_MODIFYING_METHODS_PATTERN.matcher(method).matches()) {
mType = MethodType.NON_MODIFYING_METHOD;
}
RestCsrfPreventionStrategy strategy;
switch (mType) {
case NON_MODIFYING_METHOD:
strategy = new FetchRequest();
break;
default:
strategy = new StateChangingRequest();
break;
}
if (!strategy.apply((HttpServletRequest) request, (HttpServletResponse) response)) {
return;
}
}
chain.doFilter(request, response);
}
private abstract static class RestCsrfPreventionStrategy {
abstract boolean apply(HttpServletRequest request, HttpServletResponse response)
throws IOException;
protected String extractNonceFromRequestHeader(HttpServletRequest request, String key) {
return request.getHeader(key);
}
protected String[] extractNonceFromRequestParams(HttpServletRequest request, String key) {
return request.getParameterValues(key);
}
protected void storeNonceToResponse(HttpServletResponse response, String key, String value) {
response.setHeader(key, value);
}
protected String extractNonceFromSession(HttpSession session, String key) {
return session == null ? null : (String) session.getAttribute(key);
}
protected void storeNonceToSession(HttpSession session, String key, Object value) {
session.setAttribute(key, value);
}
}
private class StateChangingRequest extends RestCsrfPreventionStrategy {
@Override
public boolean apply(HttpServletRequest request, HttpServletResponse response)
throws IOException {
if (isValidStateChangingRequest(
extractNonceFromRequest(request),
extractNonceFromSession(request.getSession(false),
Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))) {
return true;
}
storeNonceToResponse(response, Constants.CSRF_REST_NONCE_HEADER_NAME,
Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE);
response.sendError(getDenyStatus(),
sm.getString("restCsrfPreventionFilter.invalidNonce"));
return false;
}
private boolean isValidStateChangingRequest(String reqNonce, String sessionNonce) {
return reqNonce != null && sessionNonce != null
&& Objects.equals(reqNonce, sessionNonce);
}
private String extractNonceFromRequest(HttpServletRequest request) {
String nonceFromRequest = extractNonceFromRequestHeader(request,
Constants.CSRF_REST_NONCE_HEADER_NAME);
if ((nonceFromRequest == null || Objects.equals("", nonceFromRequest))
&& !getPathsAcceptingParams().isEmpty()
&& getPathsAcceptingParams().contains(getRequestedPath(request))) {
nonceFromRequest = extractNonceFromRequestParams(request);
}
return nonceFromRequest;
}
private String extractNonceFromRequestParams(HttpServletRequest request) {
String[] params = extractNonceFromRequestParams(request,
Constants.CSRF_REST_NONCE_HEADER_NAME);
if (params != null && params.length > 0) {
String nonce = params[0];
for (String param : params) {
if (!Objects.equals(param, nonce)) {
return null;
}
}
return nonce;
}
return null;
}
}
private class FetchRequest extends RestCsrfPreventionStrategy {
@Override
public boolean apply(HttpServletRequest request, HttpServletResponse response) {
if (Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE
.equalsIgnoreCase(extractNonceFromRequestHeader(request,
Constants.CSRF_REST_NONCE_HEADER_NAME))) {
String nonceFromSessionStr = extractNonceFromSession(request.getSession(false),
Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME);
if (nonceFromSessionStr == null) {
nonceFromSessionStr = generateNonce();
storeNonceToSession(Objects.requireNonNull(request.getSession(true)),
Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME, nonceFromSessionStr);
}
storeNonceToResponse(response, Constants.CSRF_REST_NONCE_HEADER_NAME,
nonceFromSessionStr);
}
return true;
}
}
/**
* A comma separated list of URLs that can accept nonces via request
* parameter 'X-CSRF-Token'. For use cases when a nonce information cannot
* be provided via header, one can provide it via request parameters. If
* there is a X-CSRF-Token header, it will be taken with preference over any
* parameter with the same name in the request. Request parameters cannot be
* used to fetch new nonce, only header.
*
* @param pathsList
* Comma separated list of URLs to be configured as paths
* accepting request parameters with nonce information.
*/
public void setPathsAcceptingParams(String pathsList) {
if (pathsList != null) {
for (String element : pathsList.split(pathsDelimiter)) {
pathsAcceptingParams.add(element.trim());
}
}
}
public Set<String> getPathsAcceptingParams() {
return pathsAcceptingParams;
}
}