/******************************************************************************* * Copyright (c) 2014 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAP AG - initial API and implementation *******************************************************************************/ package org.eclipse.orion.server.servlets; import java.io.IOException; import java.security.SecureRandom; import java.text.MessageFormat; import java.util.HashSet; import java.util.Set; import javax.servlet.*; import javax.servlet.http.*; import org.apache.commons.codec.binary.Base64; import org.eclipse.core.runtime.IStatus; import org.eclipse.orion.server.core.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A filter that implements XSRF protection via double submit cookies. */ public class XSRFPreventionFilter implements Filter { private static final String NONCES_DO_NOT_MATCH = "{0} {1} on behalf of user ''{2}'': CSRF tokens do not match: ''{3}'' does not equal ''{4}''"; private static final String NO_NONCE_IN_HEADER = "{0} {1} on behalf of user ''{2}'': missing CSRF token in header."; private static final String NO_NONCE_IN_COOKIES = "{0} {1} on behalf of user ''{2}'': missing CSRF token in cookies."; private static final Logger LOG = LoggerFactory.getLogger(XSRFPreventionFilter.class); private static final String XSRF_TOKEN = "x-csrf-token";//$NON-NLS-1$ private final Set<String> entryPointList = new HashSet<String>(); private final Set<String> exceptionList = new HashSet<String>(); private SecureRandom secureRandom; private boolean xsrfPreventionFilterDisabled = false; public void init(FilterConfig filterConfig) throws ServletException { entryPointList.add("/login");//$NON-NLS-1$ exceptionList.add("/login");//$NON-NLS-1$ exceptionList.add("/login/canaddusers");//$NON-NLS-1$ exceptionList.add("/login/form");//$NON-NLS-1$ exceptionList.add("/useremailconfirmation/cansendemails");//$NON-NLS-1$ secureRandom = new SecureRandom(); secureRandom.nextBytes(new byte[1]); String enableCSRF = PreferenceHelper.getString(ServerConstants.CONFIG_XSRF_PROTECTION_ENABLED); xsrfPreventionFilterDisabled = !Boolean.parseBoolean(enableCSRF); } public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { if (xsrfPreventionFilterDisabled) { chain.doFilter(req, resp); return; } HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String method = request.getMethod(); String path = request.getServletPath(); if (request.getPathInfo() != null) { path = path + request.getPathInfo(); } if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format("Filter called for {0} {1}. ", method, path)); } // check if nonce should be generated CookieHandler ch = new CookieHandler(request.getCookies(), XSRF_TOKEN); if (isEntryPoint(req, path) && !ch.hasNonceCookie()) { response.addCookie(new Cookie(XSRF_TOKEN, generateNonce(method, path))); } boolean doNonceCheck = !"get".equalsIgnoreCase(method) && !isException(req, path);//$NON-NLS-1$ if (doNonceCheck) { String requestNonce = request.getHeader(XSRF_TOKEN); boolean nonceValid = checkNonce(method, path, ch, requestNonce); if (!nonceValid) { logReasonForInvalidNonce(request, method, path, ch, requestNonce); prepareResponseForInvalidNonce(response); return; } } else if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format("Skipping nonce check for {0} {1}", method, path)); } chain.doFilter(request, response); } private boolean isEntryPoint(ServletRequest req, String path) { if (entryPointList.contains(path)) return true; // Self-hosting check return entryPointList.contains((String) req.getAttribute(RequestDispatcher.FORWARD_PATH_INFO)); } private boolean isException(ServletRequest req, String path) { if (exceptionList.contains(path)) return true; // Self-hosting check return exceptionList.contains((String) req.getAttribute(RequestDispatcher.FORWARD_PATH_INFO)); } public void destroy() { // do nothing } private void prepareResponseForInvalidNonce(HttpServletResponse response) throws IOException { response.setHeader(XSRF_TOKEN, "required");//$NON-NLS-1$ ServerStatus status = new ServerStatus(IStatus.ERROR, HttpServletResponse.SC_FORBIDDEN, "Access Denied", null); response.setContentType("application/json");//$NON-NLS-1$ response.setStatus(status.getHttpCode()); response.getWriter().print(status.toJSON().toString()); } private void logReasonForInvalidNonce(HttpServletRequest request, String method, String path, CookieHandler ch, String requestNonce) { if (ch.hasNonceCookie() && (requestNonce != null)) { LOG.error(MessageFormat.format(NONCES_DO_NOT_MATCH, method, path, request.getRemoteUser(), requestNonce, ch.getValue())); } else { if (!ch.hasNonceCookie()) { LOG.error(MessageFormat.format(NO_NONCE_IN_COOKIES, method, path, request.getRemoteUser())); } if (requestNonce == null) { LOG.error(MessageFormat.format(NO_NONCE_IN_HEADER, method, path, request.getRemoteUser())); } } } private boolean checkNonce(String method, String path, CookieHandler ch, String requestNonce) { boolean nonceValid = false; if (ch.hasNonceCookie()) { nonceValid = ch.getValue().equals(requestNonce); } return nonceValid; } private String generateNonce(String method, String path) { byte[] randomBytes = new byte[24]; secureRandom.nextBytes(randomBytes); String nonce = Base64.encodeBase64URLSafeString(randomBytes); if (LOG.isDebugEnabled()) { LOG.debug(MessageFormat.format("Creating nonce for {0} {1}: ''{2}''", method, path, nonce)); } return nonce; } private static class CookieHandler { private Cookie cookie; public CookieHandler(Cookie[] cookies, String name) { if (cookies == null) return; for (Cookie c : cookies) { if (name.equals(c.getName())) { cookie = c; break; } } } public String getValue() { return cookie.getValue(); } public boolean hasNonceCookie() { return cookie != null; } } }