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

Reply via email to