Author: violetagg
Date: Fri Oct 16 22:27:47 2015
New Revision: 1709120
URL: http://svn.apache.org/viewvc?rev=1709120&view=rev
Log:
Basic implementation for CSRF protection for REST.
Documentation will follow.
Added:
tomcat/trunk/java/org/apache/catalina/filters/RestCsrfPreventionFilter.java
(with props)
tomcat/trunk/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java
(with props)
Modified:
tomcat/trunk/java/org/apache/catalina/filters/Constants.java
tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties
Modified: tomcat/trunk/java/org/apache/catalina/filters/Constants.java
URL:
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/filters/Constants.java?rev=1709120&r1=1709119&r2=1709120&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/filters/Constants.java (original)
+++ tomcat/trunk/java/org/apache/catalina/filters/Constants.java Fri Oct 16
22:27:47 2015
@@ -32,4 +32,13 @@ public final class Constants {
"org.apache.catalina.filters.CSRF_NONCE";
public static final String METHOD_GET = "GET";
+
+ public static final String CSRF_REST_NONCE_HEADER_NAME = "X-CSRF-Token";
+
+ public static final String CSRF_REST_NONCE_HEADER_FETCH_VALUE = "Fetch";
+
+ public static final String CSRF_REST_NONCE_HEADER_REQUIRED_VALUE =
"Required";
+
+ public static final String CSRF_REST_NONCE_SESSION_ATTR_NAME =
+ "org.apache.catalina.filters.CSRF_REST_NONCE";
}
Modified: tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties
URL:
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties?rev=1709120&r1=1709119&r2=1709120&view=diff
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties
(original)
+++ tomcat/trunk/java/org/apache/catalina/filters/LocalStrings.properties Fri
Oct 16 22:27:47 2015
@@ -48,3 +48,4 @@ httpHeaderSecurityFilter.committed=Unabl
httpHeaderSecurityFilter.clickjack.invalid=An invalid value [{0}] was
specified for the anti click-jacking header
remoteIpFilter.invalidLocation=Failed to modify the rewrite location [{0}] to
use scheme [{1}] and port [{2}]
+restCsrfPreventionFilter.invalidNonce=CSRF nonce validation failed
\ No newline at end of file
Added:
tomcat/trunk/java/org/apache/catalina/filters/RestCsrfPreventionFilter.java
URL:
http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/filters/RestCsrfPreventionFilter.java?rev=1709120&view=auto
==============================================================================
--- tomcat/trunk/java/org/apache/catalina/filters/RestCsrfPreventionFilter.java
(added)
+++ tomcat/trunk/java/org/apache/catalina/filters/RestCsrfPreventionFilter.java
Fri Oct 16 22:27:47 2015
@@ -0,0 +1,183 @@
+/*
+ * 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.Objects;
+import java.util.function.Predicate;
+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:
+ * <ul>
+ * <li>The filter is mapped to /*</li>
+ * <li>The clients have adapted the transfer of the nonce through the
'X-CSRF-Token' header.</li>
+ * </ul>
+ *
+ * <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 static enum MethodType {
+ NON_MODIFYING_METHOD, MODIFYING_METHOD
+ }
+
+ private static final Pattern NON_MODIFYING_METHODS_PATTERN =
Pattern.compile("GET|HEAD|OPTIONS");
+ private static final Predicate<String> nonModifyingMethods = m -> m !=
null &&
+ NON_MODIFYING_METHODS_PATTERN.matcher(m).matches();
+
+ @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;
+ if (nonModifyingMethods.test(((HttpServletRequest)
request).getMethod())) {
+ 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 static interface RestCsrfPreventionStrategy {
+ static final NonceSupplier<HttpServletRequest> nonceFromRequest = (r,
k) -> r.getHeader(k);
+ static final NonceSupplier<HttpSession> nonceFromSession = (s, k) ->
Objects.isNull(s) ? null
+ : (String) s.getAttribute(k);
+
+ static final NonceConsumer<HttpServletResponse> nonceToResponse = (r,
k, v) -> r.setHeader(
+ k, v);
+ static final NonceConsumer<HttpSession> nonceToSession = (s, k, v) ->
s.setAttribute(k, v);
+
+ boolean apply(HttpServletRequest request, HttpServletResponse
response) throws IOException;
+ }
+
+ private class StateChangingRequest implements RestCsrfPreventionStrategy {
+
+ @Override
+ public boolean apply(HttpServletRequest request, HttpServletResponse
response)
+ throws IOException {
+ if (isValidStateChangingRequest(
+ nonceFromRequest.getNonce(request,
Constants.CSRF_REST_NONCE_HEADER_NAME),
+ nonceFromSession.getNonce(request.getSession(false),
Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))) {
+ return true;
+ }
+
+ nonceToResponse.setNonce(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 Objects.nonNull(reqNonce) && Objects.nonNull(sessionNonce)
+ && Objects.equals(reqNonce, sessionNonce);
+ }
+ }
+
+ private class FetchRequest implements RestCsrfPreventionStrategy {
+ private final Predicate<String> fetchRequest = s ->
Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE
+ .equalsIgnoreCase(s);
+
+ @Override
+ public boolean apply(HttpServletRequest request, HttpServletResponse
response) {
+ if (fetchRequest.test(
+ nonceFromRequest.getNonce(request,
Constants.CSRF_REST_NONCE_HEADER_NAME))) {
+ String nonceFromSessionStr =
nonceFromSession.getNonce(request.getSession(false),
+ Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME);
+ if (nonceFromSessionStr == null) {
+ nonceFromSessionStr = generateNonce();
+
nonceToSession.setNonce(Objects.requireNonNull(request.getSession(true)),
+ Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME,
nonceFromSessionStr);
+ }
+ nonceToResponse.setNonce(response,
Constants.CSRF_REST_NONCE_HEADER_NAME,
+ nonceFromSessionStr);
+ }
+ return true;
+ }
+
+ }
+
+ @FunctionalInterface
+ private static interface NonceSupplier<T> {
+ String getNonce(T supplier, String key);
+ }
+
+ @FunctionalInterface
+ private static interface NonceConsumer<T> {
+ void setNonce(T consumer, String key, String value);
+ }
+}
Propchange:
tomcat/trunk/java/org/apache/catalina/filters/RestCsrfPreventionFilter.java
------------------------------------------------------------------------------
svn:eol-style = native
Added:
tomcat/trunk/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java
URL:
http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java?rev=1709120&view=auto
==============================================================================
---
tomcat/trunk/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java
(added)
+++
tomcat/trunk/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java
Fri Oct 16 22:27:47 2015
@@ -0,0 +1,236 @@
+/*
+ * 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 javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import org.easymock.EasyMock;
+
+public class TestRestCsrfPreventionFilter {
+
+ private static final String NONCE = "nonce";
+
+ private static final String INVALID_NONCE = "invalid-nonce";
+
+ private static final String GET_METHOD = "GET";
+
+ private static final String POST_METHOD = "POST";
+
+ private RestCsrfPreventionFilter filter;
+
+ private TesterRequest request;
+
+ private TesterResponse response;
+
+ private TesterFilterChain filterChain;
+
+ private HttpSession session;
+
+ @Before
+ public void setUp() {
+ filter = new RestCsrfPreventionFilter() {
+ @Override
+ protected String generateNonce() {
+ return NONCE;
+ }
+ };
+ request = new TesterRequest();
+ response = new TesterResponse();
+ filterChain = new TesterFilterChain();
+ session = EasyMock.createMock(HttpSession.class);
+ }
+
+ @Test
+ public void testGetRequestNoSessionNoNonce() throws Exception {
+ setRequestExpectations(GET_METHOD, null, null);
+ filter.doFilter(request, response, filterChain);
+ verifyContinueChain();
+ }
+
+ @Test
+ public void testPostRequestNoSessionNoNonce() throws Exception {
+ setRequestExpectations(POST_METHOD, null, null);
+ filter.doFilter(request, response, filterChain);
+ verifyDenyResponse(HttpServletResponse.SC_FORBIDDEN);
+ }
+
+ @Test
+ public void testPostRequestSessionNoNonce1() throws Exception {
+ setRequestExpectations(POST_METHOD, session, null);
+
EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))
+ .andReturn(null);
+ EasyMock.replay(session);
+ filter.doFilter(request, response, filterChain);
+ verifyDenyResponse(HttpServletResponse.SC_FORBIDDEN);
+ EasyMock.verify(session);
+ }
+
+ @Test
+ public void testPostRequestSessionNoNonce2() throws Exception {
+ setRequestExpectations(POST_METHOD, session, null);
+
EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))
+ .andReturn(NONCE);
+ EasyMock.replay(session);
+ filter.doFilter(request, response, filterChain);
+ verifyDenyResponse(HttpServletResponse.SC_FORBIDDEN);
+ EasyMock.verify(session);
+ }
+
+ @Test
+ public void testPostRequestSessionInvalidNonce() throws Exception {
+ setRequestExpectations(POST_METHOD, session, INVALID_NONCE);
+
EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))
+ .andReturn(NONCE);
+ EasyMock.replay(session);
+ filter.doFilter(request, response, filterChain);
+ verifyDenyResponse(HttpServletResponse.SC_FORBIDDEN);
+ EasyMock.verify(session);
+ }
+
+ @Test
+ public void testPostRequestSessionValidNonce() throws Exception {
+ setRequestExpectations(POST_METHOD, session, NONCE);
+
EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))
+ .andReturn(NONCE);
+ EasyMock.replay(session);
+ filter.doFilter(request, response, filterChain);
+ verifyContinueChain();
+ EasyMock.verify(session);
+ }
+
+ @Test
+ public void testGetFetchRequestSessionNoNonce() throws Exception {
+ setRequestExpectations(GET_METHOD, session,
Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE);
+
EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))
+ .andReturn(null);
+ session.setAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME,
NONCE);
+ EasyMock.expectLastCall();
+ EasyMock.replay(session);
+ filter.doFilter(request, response, filterChain);
+ verifyContinueChainNonceAvailable();
+ EasyMock.verify(session);
+ }
+
+ @Test
+ public void testPostFetchRequestSessionNoNonce() throws Exception {
+ setRequestExpectations(POST_METHOD, session,
Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE);
+
EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))
+ .andReturn(null);
+ EasyMock.replay(session);
+ filter.doFilter(request, response, filterChain);
+ verifyDenyResponse(HttpServletResponse.SC_FORBIDDEN);
+ EasyMock.verify(session);
+ }
+
+ @Test
+ public void testGetFetchRequestSessionNonce() throws Exception {
+ setRequestExpectations(GET_METHOD, session,
Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE);
+
EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))
+ .andReturn(NONCE);
+ EasyMock.replay(session);
+ filter.doFilter(request, response, filterChain);
+ verifyContinueChainNonceAvailable();
+ EasyMock.verify(session);
+ }
+
+ @Test
+ public void testPostFetchRequestSessionNonce() throws Exception {
+ setRequestExpectations(POST_METHOD, session,
Constants.CSRF_REST_NONCE_HEADER_FETCH_VALUE);
+
EasyMock.expect(session.getAttribute(Constants.CSRF_REST_NONCE_SESSION_ATTR_NAME))
+ .andReturn(NONCE);
+ EasyMock.replay(session);
+ filter.doFilter(request, response, filterChain);
+ verifyDenyResponse(HttpServletResponse.SC_FORBIDDEN);
+ EasyMock.verify(session);
+ }
+
+ @Test
+ public void testPostRequestCustomDenyStatus() throws Exception {
+ setRequestExpectations(POST_METHOD, null, null);
+ filter.setDenyStatus(HttpServletResponse.SC_BAD_REQUEST);
+ filter.doFilter(request, response, filterChain);
+ verifyDenyResponse(HttpServletResponse.SC_BAD_REQUEST);
+ }
+
+ private void setRequestExpectations(String method, HttpSession session,
String headerValue) {
+ request.setMethod(method);
+ request.setSession(session);
+ request.setHeader(Constants.CSRF_REST_NONCE_HEADER_NAME, headerValue);
+ }
+
+ private void verifyContinueChain() {
+ assertTrue(filterChain.isVisited());
+ }
+
+ private void verifyContinueChainNonceAvailable() {
+
assertTrue(NONCE.equals(response.getHeader(Constants.CSRF_REST_NONCE_HEADER_NAME)));
+ verifyContinueChain();
+ }
+
+ private void verifyDenyResponse(int statusCode) {
+
assertTrue(Constants.CSRF_REST_NONCE_HEADER_REQUIRED_VALUE.equals(response
+ .getHeader(Constants.CSRF_REST_NONCE_HEADER_NAME)));
+ assertTrue(statusCode == response.getStatus());
+ assertTrue(!filterChain.isVisited());
+ }
+
+ private static class TesterFilterChain implements FilterChain {
+ private boolean visited = false;
+
+ @Override
+ public void doFilter(ServletRequest request, ServletResponse response)
throws IOException,
+ ServletException {
+ visited = true;
+ }
+
+ boolean isVisited() {
+ return visited;
+ }
+ }
+
+ private static class TesterRequest extends TesterHttpServletRequest {
+ private HttpSession session;
+
+ void setSession(HttpSession session) {
+ this.session = session;
+ }
+
+ @Override
+ public HttpSession getSession(boolean create) {
+ return session;
+ }
+ }
+
+ private static class TesterResponse extends TesterHttpServletResponse {
+ @Override
+ public void sendError(int status, String message) throws IOException {
+ setStatus(status);
+ }
+ }
+}
Propchange:
tomcat/trunk/test/org/apache/catalina/filters/TestRestCsrfPreventionFilter.java
------------------------------------------------------------------------------
svn:eol-style = native
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]