From d97571cce4a65a2b3141588dea892dcd2e3daf8f Mon Sep 17 00:00:00 2001 From: markt Date: Thu, 24 Jun 2010 09:57:02 +0000 Subject: [PATCH] Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=49476 CSRF protection was preventing access to session expiration features Also: - Switch Manager app to generic CSRF protection - Add support for multiple nonces to CSRF filter - Improve 403 page - Don't open JSP pages in session expiration in a new window - makes CSRF prevention a real pain git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@957478 13f79535-47bb-0310-9956-ffa450edef68 --- .../catalina/filters/CsrfPreventionFilter.java | 54 ++++++++-- .../catalina/manager/HTMLManagerServlet.java | 99 +++++------------- .../catalina/manager/LocalStrings.properties | 2 - webapps/docs/config/filter.xml | 8 ++ webapps/manager/403.jsp | 17 +++- webapps/manager/WEB-INF/jsp/sessionDetail.jsp | 113 +++++++++++++-------- webapps/manager/WEB-INF/jsp/sessionsList.jsp | 11 +- webapps/manager/WEB-INF/web.xml | 15 +++ 8 files changed, 189 insertions(+), 130 deletions(-) diff --git a/java/org/apache/catalina/filters/CsrfPreventionFilter.java b/java/org/apache/catalina/filters/CsrfPreventionFilter.java index f72a6c617..73dffe5c1 100644 --- a/java/org/apache/catalina/filters/CsrfPreventionFilter.java +++ b/java/org/apache/catalina/filters/CsrfPreventionFilter.java @@ -19,6 +19,8 @@ package org.apache.catalina.filters; import java.io.IOException; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; import java.util.Random; import java.util.Set; @@ -51,6 +53,8 @@ public class CsrfPreventionFilter extends FilterBase { private final Random randomSource = new Random(); private final Set entryPoints = new HashSet(); + + private final int nonceCacheSize = 5; @Override protected Log getLogger() { @@ -98,24 +102,30 @@ public class CsrfPreventionFilter extends FilterBase { } } + @SuppressWarnings("unchecked") + LruCache nonceCache = + (LruCache) req.getSession(true).getAttribute( + Constants.CSRF_NONCE_SESSION_ATTR_NAME); + if (!skipNonceCheck) { String previousNonce = req.getParameter(Constants.CSRF_NONCE_REQUEST_PARAM); - String expectedNonce = - (String) req.getSession(true).getAttribute( - Constants.CSRF_NONCE_SESSION_ATTR_NAME); - - if (expectedNonce != null && - !expectedNonce.equals(previousNonce)) { + + if (nonceCache != null && !nonceCache.contains(previousNonce)) { res.sendError(HttpServletResponse.SC_FORBIDDEN); return; } } + if (nonceCache == null) { + nonceCache = new LruCache(nonceCacheSize); + req.getSession().setAttribute( + Constants.CSRF_NONCE_SESSION_ATTR_NAME, nonceCache); + } + String newNonce = generateNonce(); - req.getSession(true).setAttribute( - Constants.CSRF_NONCE_SESSION_ATTR_NAME, newNonce); + nonceCache.add(newNonce); wResponse = new CsrfResponseWrapper(res, newNonce); } else { @@ -225,4 +235,32 @@ public class CsrfPreventionFilter extends FilterBase { return (sb.toString()); } } + + private static class LruCache { + + // Although the internal implementation uses a Map, this cache + // implementation is only concerned with the keys. + private final Map cache; + + public LruCache(final int cacheSize) { + cache = new LinkedHashMap() { + private static final long serialVersionUID = 1L; + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > cacheSize) { + return true; + } + return false; + } + }; + } + + public void add(T key) { + cache.put(key, null); + } + + public boolean contains(T key) { + return cache.containsKey(key); + } + } } diff --git a/java/org/apache/catalina/manager/HTMLManagerServlet.java b/java/org/apache/catalina/manager/HTMLManagerServlet.java index 76ce2e3b2..e72651d6c 100644 --- a/java/org/apache/catalina/manager/HTMLManagerServlet.java +++ b/java/org/apache/catalina/manager/HTMLManagerServlet.java @@ -84,10 +84,6 @@ public final class HTMLManagerServlet extends ManagerServlet { protected static final String APPLICATION_MESSAGE = "message"; protected static final String APPLICATION_ERROR = "error"; - protected static final String NONCE_SESSION = - "org.apache.catalina.manager.NONCE"; - protected static final String NONCE_REQUEST = "nonce"; - protected static final String sessionsListJspPath = "/WEB-INF/jsp/sessionsList.jsp"; protected static final String sessionDetailJspPath = "/WEB-INF/jsp/sessionDetail.jsp"; @@ -177,31 +173,12 @@ public final class HTMLManagerServlet extends ManagerServlet { String deployPath = request.getParameter("deployPath"); String deployConfig = request.getParameter("deployConfig"); String deployWar = request.getParameter("deployWar"); - String requestNonce = request.getParameter(NONCE_REQUEST); // Prepare our output writer to generate the response message response.setContentType("text/html; charset=" + Constants.CHARSET); String message = ""; - // Check nonce - // There *must* be a nonce in the session before any POST is processed - HttpSession session = request.getSession(); - String sessionNonce = (String) session.getAttribute(NONCE_SESSION); - if (sessionNonce == null) { - message = sm.getString("htmlManagerServlet.noNonce", command); - // Reset the command - command = null; - } else { - if (!sessionNonce.equals(requestNonce)) { - // Nonce mis-match. - message = - sm.getString("htmlManagerServlet.nonceMismatch", command); - // Reset the command - command = null; - } - } - if (command == null || command.length() == 0) { // No command == list // List always displayed -> do nothing @@ -417,9 +394,6 @@ public final class HTMLManagerServlet extends ManagerServlet { log("list: Listing contexts for virtual host '" + host.getName() + "'"); - String newNonce = generateNonce(); - request.getSession().setAttribute(NONCE_SESSION, newNonce); - PrintWriter writer = response.getWriter(); // HTML Header Section @@ -509,12 +483,12 @@ public final class HTMLManagerServlet extends ManagerServlet { Map.Entry entry = iterator.next(); String displayPath = entry.getKey(); String contextPath = entry.getValue(); - Context context = (Context) host.findChild(contextPath); + Context ctxt = (Context) host.findChild(contextPath); if (displayPath.equals("")) { displayPath = "/"; } - if (context != null ) { + if (ctxt != null ) { try { isDeployed = isDeployed(contextPath); } catch (Exception e) { @@ -525,17 +499,17 @@ public final class HTMLManagerServlet extends ManagerServlet { args = new Object[7]; args[0] = URL_ENCODER.encode(displayPath); args[1] = displayPath; - args[2] = context.getDisplayName(); + args[2] = ctxt.getDisplayName(); if (args[2] == null) { args[2] = " "; } - args[3] = new Boolean(context.getAvailable()); + args[3] = new Boolean(ctxt.getAvailable()); args[4] = response.encodeURL (request.getContextPath() + "/html/sessions?path=" + URL_ENCODER.encode(displayPath)); - if (context.getManager() != null) { + if (ctxt.getManager() != null) { args[5] = new Integer - (context.getManager().getActiveSessions()); + (ctxt.getManager().getActiveSessions()); } else { args[5] = new Integer(0); } @@ -545,7 +519,7 @@ public final class HTMLManagerServlet extends ManagerServlet { writer.print (MessageFormat.format(APPS_ROW_DETAILS_SECTION, args)); - args = new Object[15]; + args = new Object[14]; args[0] = response.encodeURL (request.getContextPath() + "/html/start?path=" + URL_ENCODER.encode(displayPath)); @@ -568,27 +542,26 @@ public final class HTMLManagerServlet extends ManagerServlet { "/html/expire?path=" + URL_ENCODER.encode(displayPath)); args[9] = appsExpire; args[10] = sm.getString("htmlManagerServlet.expire.explain"); - Manager manager = context.getManager(); + Manager manager = ctxt.getManager(); if (manager == null) { args[11] = sm.getString("htmlManagerServlet.noManager"); } else { args[11] = new Integer( - context.getManager().getMaxInactiveInterval()/60); + ctxt.getManager().getMaxInactiveInterval()/60); } args[12] = sm.getString("htmlManagerServlet.expire.unit"); args[13] = highlightColor; - args[14] = newNonce; - if (context.getPath().equals(this.context.getPath())) { + if (ctxt.getPath().equals(this.context.getPath())) { writer.print(MessageFormat.format( MANAGER_APP_ROW_BUTTON_SECTION, args)); - } else if (context.getAvailable() && isDeployed) { + } else if (ctxt.getAvailable() && isDeployed) { writer.print(MessageFormat.format( STARTED_DEPLOYED_APPS_ROW_BUTTON_SECTION, args)); - } else if (context.getAvailable() && !isDeployed) { + } else if (ctxt.getAvailable() && !isDeployed) { writer.print(MessageFormat.format( STARTED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION, args)); - } else if (!context.getAvailable() && isDeployed) { + } else if (!ctxt.getAvailable() && isDeployed) { writer.print(MessageFormat.format( STOPPED_DEPLOYED_APPS_ROW_BUTTON_SECTION, args)); } else { @@ -600,7 +573,7 @@ public final class HTMLManagerServlet extends ManagerServlet { } // Deploy Section - args = new Object[8]; + args = new Object[7]; args[0] = sm.getString("htmlManagerServlet.deployTitle"); args[1] = sm.getString("htmlManagerServlet.deployServer"); args[2] = response.encodeURL(request.getContextPath() + "/html/deploy"); @@ -608,26 +581,23 @@ public final class HTMLManagerServlet extends ManagerServlet { args[4] = sm.getString("htmlManagerServlet.deployConfig"); args[5] = sm.getString("htmlManagerServlet.deployWar"); args[6] = sm.getString("htmlManagerServlet.deployButton"); - args[7] = newNonce; writer.print(MessageFormat.format(DEPLOY_SECTION, args)); - args = new Object[5]; + args = new Object[4]; args[0] = sm.getString("htmlManagerServlet.deployUpload"); args[1] = response.encodeURL(request.getContextPath() + "/html/upload"); args[2] = sm.getString("htmlManagerServlet.deployUploadFile"); args[3] = sm.getString("htmlManagerServlet.deployButton"); - args[4] = newNonce; writer.print(MessageFormat.format(UPLOAD_SECTION, args)); // Diagnostics section - args = new Object[6]; + args = new Object[5]; args[0] = sm.getString("htmlManagerServlet.diagnosticsTitle"); args[1] = sm.getString("htmlManagerServlet.diagnosticsLeak"); args[2] = response.encodeURL( request.getContextPath() + "/html/findleaks"); - args[3] = newNonce; - args[4] = sm.getString("htmlManagerServlet.diagnosticsLeakWarning"); - args[5] = sm.getString("htmlManagerServlet.diagnosticsLeakButton"); + args[3] = sm.getString("htmlManagerServlet.diagnosticsLeakWarning"); + args[4] = sm.getString("htmlManagerServlet.diagnosticsLeakButton"); writer.print(MessageFormat.format(DIAGNOSTICS_SECTION, args)); // Server Header Section @@ -870,12 +840,12 @@ public final class HTMLManagerServlet extends ManagerServlet { String searchPath = path; if( path.equals("/") ) searchPath = ""; - Context context = (Context) host.findChild(searchPath); - if (null == context) { + Context ctxt = (Context) host.findChild(searchPath); + if (null == ctxt) { throw new IllegalArgumentException(sm.getString("managerServlet.noContext", RequestUtil.filter(path))); } - Session[] sessions = context.getManager().findSessions(); + Session[] sessions = ctxt.getManager().findSessions(); return sessions; } protected Session getSessionForPathAndId(String path, String id) throws IOException { @@ -886,12 +856,12 @@ public final class HTMLManagerServlet extends ManagerServlet { String searchPath = path; if( path.equals("/") ) searchPath = ""; - Context context = (Context) host.findChild(searchPath); - if (null == context) { + Context ctxt = (Context) host.findChild(searchPath); + if (null == ctxt) { throw new IllegalArgumentException(sm.getString("managerServlet.noContext", RequestUtil.filter(path))); } - Session session = context.getManager().findSession(id); + Session session = ctxt.getManager().findSession(id); return session; } @@ -955,7 +925,7 @@ public final class HTMLManagerServlet extends ManagerServlet { resp.setHeader("Cache-Control", "no-cache,no-store,max-age=0"); // HTTP 1.1 resp.setDateHeader("Expires", 0); // 0 means now req.setAttribute("currentSession", session); - getServletContext().getRequestDispatcher(sessionDetailJspPath).include(req, resp); + getServletContext().getRequestDispatcher(resp.encodeURL(sessionDetailJspPath)).include(req, resp); } /** @@ -1152,7 +1122,7 @@ public final class HTMLManagerServlet extends ManagerServlet { " {2}\n" + " {3}\n" + " " + - "{5}\n"; + "{5}\n"; private static final String MANAGER_APP_ROW_BUTTON_SECTION = " \n" + @@ -1167,7 +1137,6 @@ public final class HTMLManagerServlet extends ManagerServlet { " \n" + "
\n" + " \n" + - " " + "   {10}  {12} \n" + " \n" + "
\n" + @@ -1178,15 +1147,12 @@ public final class HTMLManagerServlet extends ManagerServlet { " \n" + "  {1} \n" + "
" + - " " + " " + "
\n" + "
" + - " " + " " + "
\n" + "
" + - " " + "
\n" + " \n" + @@ -1194,7 +1160,6 @@ public final class HTMLManagerServlet extends ManagerServlet { " \n" + "
\n" + " \n" + - " " + "   {10}  {12} \n" + " \n" + "
\n" + @@ -1204,13 +1169,11 @@ public final class HTMLManagerServlet extends ManagerServlet { private static final String STOPPED_DEPLOYED_APPS_ROW_BUTTON_SECTION = " \n" + "
" + - " " + " " + "
\n" + "  {3} \n" + "  {5} \n" + "
" + - " " + " " + "
\n" + " \n" + @@ -1220,11 +1183,9 @@ public final class HTMLManagerServlet extends ManagerServlet { " \n" + "  {1} \n" + "
" + - " " + " " + "
\n" + "
" + - " " + " " + "
\n" + "  {7} \n" + @@ -1234,7 +1195,6 @@ public final class HTMLManagerServlet extends ManagerServlet { private static final String STOPPED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION = " \n" + "
" + - " " + " " + "
\n" + "  {3} \n" + @@ -1256,7 +1216,6 @@ public final class HTMLManagerServlet extends ManagerServlet { "\n" + " \n" + "
\n" + - "" + "\n" + "\n" + " - - - - - - - - <%--tfoot> - - - - - + + + + + + + + + <%--tfoot> + + + + + <% attributeNamesEnumeration = currentHttpSession.getAttributeNames(); while (attributeNamesEnumeration.hasMoreElements()) { - String attributeName = (String) attributeNamesEnumeration.nextElement(); + String attributeName = (String) attributeNamesEnumeration.nextElement(); %> - - - - - + + + + + <% } // end while %> - +
\n" + @@ -1303,7 +1262,6 @@ public final class HTMLManagerServlet extends ManagerServlet { " \n" + "\n" + - "" + "\n" + "\n" + " \n" + "
\n" + @@ -1338,14 +1296,13 @@ public final class HTMLManagerServlet extends ManagerServlet { "
\n" + "\n" + - "" + "\n" + "\n" + " \n" + " \n" + "\n" + "
\n" + - " \n" + + " \n" + " \n" + - " {4}\n" + + " {3}\n" + "
\n" + diff --git a/java/org/apache/catalina/manager/LocalStrings.properties b/java/org/apache/catalina/manager/LocalStrings.properties index ae3f389f3..f764bdac1 100644 --- a/java/org/apache/catalina/manager/LocalStrings.properties +++ b/java/org/apache/catalina/manager/LocalStrings.properties @@ -56,8 +56,6 @@ htmlManagerServlet.list=List Applications htmlManagerServlet.manager=Manager htmlManagerServlet.messageLabel=Message: htmlManagerServlet.noManager=- -htmlManagerServlet.noNonce=FAIL: No nonce found in session. Command \"{0}\" was ignored -htmlManagerServlet.nonceMismatch=FAIL: Nonce mismatch. Command \"{0}\" was ignored. htmlManagerServlet.serverJVMVendor=JVM Vendor htmlManagerServlet.serverJVMVersion=JVM Version htmlManagerServlet.serverOSArch=OS Architecture diff --git a/webapps/docs/config/filter.xml b/webapps/docs/config/filter.xml index fed881044..b1fdea615 100644 --- a/webapps/docs/config/filter.xml +++ b/webapps/docs/config/filter.xml @@ -127,6 +127,14 @@ any security sensitive actions.

+ +

The number of previously issued nonces that will be cached on a LRU + basis to support parallel requests, limited use of the refresh and back + in the browser and similar behaviors that may result in the submission + of a previous nonce rather than the current one. If not set, the default + value of 5 will be used.

+
+ diff --git a/webapps/manager/403.jsp b/webapps/manager/403.jsp index 7c4b5d2a2..ecaf09761 100644 --- a/webapps/manager/403.jsp +++ b/webapps/manager/403.jsp @@ -33,7 +33,22 @@

403 Access Denied

- You are not authorized to view this page. If you have not changed + You are not authorized to view this page. +

+

+ If you have already configured the manager application to allow access and + you have used your browsers back button, used a saved bookmark or similar + then you may have triggered the cross-site request forgery (CSRF) protection + that has been enabled for the HTML interface of the Manager application. You + will need to reset this protection by returning to the + main manager page. Once you + return to this page you will be able to continue using the manager + appliction's HTML interface normally. If you continue to see this access + denied message, check that you have the necessary permissions to access this + application. +

+

+ If you have not changed any configuration files, please examine the file conf/tomcat-users.xml in your installation. That file must contain the credentials to let you use this webapp. diff --git a/webapps/manager/WEB-INF/jsp/sessionDetail.jsp b/webapps/manager/WEB-INF/jsp/sessionDetail.jsp index 654c5cbe3..5f6894819 100644 --- a/webapps/manager/WEB-INF/jsp/sessionDetail.jsp +++ b/webapps/manager/WEB-INF/jsp/sessionDetail.jsp @@ -31,18 +31,19 @@ Session currentSession = (Session)request.getAttribute("currentSession"); HttpSession currentHttpSession = currentSession.getSession(); String currentSessionId = currentSession.getId(); - String submitUrl = ((HttpServletRequest)pageContext.getRequest()).getRequestURL().toString(); + String submitUrl = response.encodeURL(((HttpServletRequest) + pageContext.getRequest()).getRequestURL().toString()); %> - - - - - - - - Sessions Administration: details for <%= currentSessionId %> + + + + + + + + Sessions Administration: details for <%= currentSessionId %>

Details for Session <%= JspHelper.escapeXml(currentSessionId) %>

@@ -86,7 +87,14 @@
-

+ +
+ + + + +
+
<%= JspHelper.escapeXml(request.getAttribute("error")) %>
<%= JspHelper.escapeXml(request.getAttribute("message")) %>
@@ -95,52 +103,67 @@ <% int nAttributes = 0; Enumeration attributeNamesEnumeration = currentHttpSession.getAttributeNames(); while (attributeNamesEnumeration.hasMoreElements()) { - attributeNamesEnumeration.nextElement(); - ++nAttributes; + attributeNamesEnumeration.nextElement(); + ++nAttributes; } %> -
<%= JspHelper.formatNumber(nAttributes) %> attributes
Remove AttributeAttribute nameAttribute value
- TODO: set Max Inactive Interval on sessions -
<%= JspHelper.formatNumber(nAttributes) %> attributes
Remove AttributeAttribute nameAttribute value
+ TODO: set Max Inactive Interval on sessions +
<%= JspHelper.escapeXml(attributeName) %><% Object attributeValue = currentHttpSession.getAttribute(attributeName); %>"><%= JspHelper.escapeXml(attributeValue) %>
+
+
+ + + + + +
+
+
<%= JspHelper.escapeXml(attributeName) %><% Object attributeValue = currentHttpSession.getAttribute(attributeName); %>"><%= JspHelper.escapeXml(attributeValue) %>
-

+
+

+ + +

+
<%--div style="display: none;">

- Valid HTML 4.01! - Valid XHTML 1.0! - Valid XHTML 1.1! + Valid HTML 4.01! + Valid XHTML 1.0! + Valid XHTML 1.1!

diff --git a/webapps/manager/WEB-INF/jsp/sessionsList.jsp b/webapps/manager/WEB-INF/jsp/sessionsList.jsp index 857b6279c..2f2d78b62 100644 --- a/webapps/manager/WEB-INF/jsp/sessionsList.jsp +++ b/webapps/manager/WEB-INF/jsp/sessionsList.jsp @@ -26,7 +26,8 @@ <% String path = (String) request.getAttribute("path"); - String submitUrl = ((HttpServletRequest)pageContext.getRequest()).getRequestURI() + "?path=" + path; + String submitUrl = response.encodeURL(((HttpServletRequest) + pageContext.getRequest()).getRequestURI() + "?path=" + path); Collection activeSessions = (Collection) request.getAttribute("activeSessions"); %> @@ -99,7 +100,7 @@ %> -<%= JspHelper.escapeXml(currentSessionId) %> +<%= JspHelper.escapeXml(currentSessionId) %> <%= JspHelper.guessDisplayLocaleFromSession(currentSession) %> <%= JspHelper.guessDisplayUserFromSession(currentSession) %> @@ -118,7 +119,11 @@ -

+
+

+ +

+
<%--div style="display: none;">

diff --git a/webapps/manager/WEB-INF/web.xml b/webapps/manager/WEB-INF/web.xml index 3ea5f5497..29864abb0 100644 --- a/webapps/manager/WEB-INF/web.xml +++ b/webapps/manager/WEB-INF/web.xml @@ -83,6 +83,21 @@ /html/* + + CSRF + org.apache.catalina.filters.CsrfPreventionFilter + + entryPoints + /html,/html/,/html/list + + + + + CSRF + HTMLManager + jsp + + -- 2.11.0