/* * Copyright (c) 2014 Red Hat, Inc. * * Licensed 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.ovirt.engine.api.restapi.security; import java.io.IOException; import java.util.Enumeration; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; 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; import org.apache.http.HeaderElement; import org.apache.http.message.BasicHeaderValueParser; import org.ovirt.engine.core.common.config.Config; import org.ovirt.engine.core.common.config.ConfigValues; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This filter implements CSRF protection. In order to activate the protection the client system has to activate it * adding the {@code csrf-protection} element to the {@code Prefer} header sent in the first request for the session: * * <pre> * GET /ovirt-engine/api HTTP/1.1 * Authorization: Basic P/c1qcSSGuTlxUCTEUCosZfZ * Host: ovirt.example.com * Prefer: persistent-auth, csrf-protection * </pre> * * The server will then require that the session identifier is sent with every request, inside the {@code JSESSIONID} * header: * * <pre> * GET /ovirt-engine/api HTTP/1.1 * Cookie: JSESSIONID=y+FXYivGm2rdajrNhTRatNjl * Prefer: persistent-auth, csrf-protection * JSESSIONID: y+FXYivGm2rdajrNhTRatNjl * </pre> * * Requests for sessions where protection has been enabled will be checked. If the session identifier header isn't * provided or incorrect they will be rejected with code 403 (forbidden) and logged as warnings. */ @SuppressWarnings("unused") public class CSRFProtectionFilter implements Filter { /** * The log used by the filter. */ private static final Logger log = LoggerFactory.getLogger(CSRFProtectionFilter.class); /** * The name of the header that contains preferences. */ private static final String PREFER_HEADER = "Prefer"; /** * The name of the header element that is used to request protection. */ private static final String PREFER_ELEMENT = "csrf-protection"; /** * The name of the header that contains the session id. */ private static final String SESSION_ID_HEADER = "JSESSIONID"; /** * The name of the session attribute that contains the boolean flag that indicates if the protection is enabled * for the session. */ private static final String ENABLED_ATTRIBUTE = CSRFProtectionFilter.class.getName() + ".enabled"; @Override public void init(FilterConfig config) throws ServletException { } @Override public void destroy() { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); } private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // If protection is globally disabled then we don't need to do anything else, jump directly to the next filter // in the chain: boolean enabled = Config.getValue(ConfigValues.CSRFProtection); if (!enabled) { chain.doFilter(request, response); return; } // If there is already a session then we need to process it immediately, before letting other filters or the // application see or touch the request: HttpSession session = request.getSession(false); if (session != null) { doFilterExistingSession(session, request, response, chain); return; } // At this point we know that protection is globally enabled, and that there isn't a session already created. So // we should first let the other filters and the application do their work. As a result a new session may be // created. In that case we need to check if protection has been requested for that session and store the result // for use in future requests. try { chain.doFilter(request, response); } finally { session = request.getSession(false); if (session != null) { enabled = isProtectionRequested(request); session.setAttribute(ENABLED_ATTRIBUTE, enabled); } } } private void doFilterExistingSession(HttpSession session, HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // Check if the protection is enabled for this session, if it isn't then jump to the next filter: boolean enabled = (Boolean) session.getAttribute(ENABLED_ATTRIBUTE); if (!enabled) { chain.doFilter(request, response); return; } // Check if the request contains a session id header, if it doesn't then it must be rejected immediately: String sessionIdHeader = request.getHeader(SESSION_ID_HEADER); if (sessionIdHeader == null) { log.warn( "Request for path \"{}\" from IP address {} has been rejected because CSRF protection is enabled " + "for the session but the the session id header \"{}\" hasn't been provided.", request.getContextPath() + request.getPathInfo(), request.getRemoteAddr(), SESSION_ID_HEADER ); response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } // Check if the actual session id matches the session id header: String actualSessionId = session.getId(); if (!sessionIdHeader.equals(actualSessionId)) { log.warn( "Request for path \"{}\" from IP address {} has been rejected because CSRF protection is enabled " + "for the session but the value of the session id header \"{}\" doesn't match the actual session " + "id.", request.getContextPath() + request.getPathInfo(), request.getRemoteAddr(), SESSION_ID_HEADER ); response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } // Everything is OK, let the request go to the next filter: chain.doFilter(request, response); } /** * Checks if the headers contained in the given request indicate that the user wants to enable protection. This * means checking if the {@code Prefer} header exists and has at least one {@code csrf-protection} element. For * example: * * <pre> * GET /ovirt-engine/api HTTP/1.1 * Host: ovirt.example.com * Prefer: persistent-auth, csrf-protection * </pre> * * @param request the HTTP request to check * @return {@code true} if the request contains headers that indicate that protection should be enabled, * {@code false} otherwise */ private boolean isProtectionRequested(HttpServletRequest request) { Enumeration<String> headerValues = request.getHeaders(PREFER_HEADER); while (headerValues.hasMoreElements()) { String headerValue = headerValues.nextElement(); HeaderElement[] headerElements = BasicHeaderValueParser.parseElements(headerValue, null); for (HeaderElement headerElement : headerElements) { String elementName = headerElement.getName(); if (PREFER_ELEMENT.equalsIgnoreCase(elementName)) { return true; } } } return false; } }