Author: markt Date: Tue Apr 8 22:19:46 2014 New Revision: 1585853 URL: http://svn.apache.org/r1585853 Log: Redefine the globalXsltFile initialisation parameter of the DefaultServlet as relative to CATALINA_BASE/conf or CATALINA_HOME/conf. Prevent user supplied XSLTs used by the DefaultServlet from defining external entities.
Modified: tomcat/tc6.0.x/trunk/STATUS.txt tomcat/tc6.0.x/trunk/conf/web.xml tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/DefaultServlet.java tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/LocalStrings.properties tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml tomcat/tc6.0.x/trunk/webapps/docs/default-servlet.xml Modified: tomcat/tc6.0.x/trunk/STATUS.txt URL: http://svn.apache.org/viewvc/tomcat/tc6.0.x/trunk/STATUS.txt?rev=1585853&r1=1585852&r2=1585853&view=diff ============================================================================== --- tomcat/tc6.0.x/trunk/STATUS.txt (original) +++ tomcat/tc6.0.x/trunk/STATUS.txt Tue Apr 8 22:19:46 2014 @@ -28,37 +28,6 @@ None PATCHES PROPOSED TO BACKPORT: [ New proposals should be added at the end of the list ] -* Redefine the <code>globalXsltFile</code> initialisation parameter of the - DefaultServlet as relative to CATALINA_BASE/conf or CATALINA_HOME/conf. - Prevent user supplied XSLTs used by the DefaultServlet from defining external - entities. - http://people.apache.org/~markt/patches/2014-03-17-globalXsltFile-tc6-v1.patch - +1: markt, kkolinko, remm - -1: schultz: The idea of the patch is fine: I'm actually +1. - I have some small nits: - 2. Two instances of swallowing IOException - when closing File streams. We should at least log a warning. - The case of InputStream vs OutputStream is not relevant: - a stream left open should be logged. Honestly, it will - pretty much never happen, but that's no excuse not to log - a potential problem. - kkolinko: - Re 2.: - Those are input streams that are read, not written. Nothing - should really happen when those are closed. - remm: no need to add i18n for something that will not happen - markt: Not logging exceptions on close is consistent with the code prior - to this patch. While I'm not convinced of the need, I'm happy to - replace each of those two // Ignore lines with these three lines: - if (debug > 10) { - log(e.getMessage(), e); - } - - schultz: I'm +1 if there's a debug message. I would prefer a WARN or - something like that for an allegedly impossible situation, but - I'll settle for debug ;) - -1: - * Fix http://issues.apache.org/bugzilla/show_bug.cgi?id=56283 Add Java 8 support to Jasper's default configuration http://people.apache.org/~markt/patches/2014-03-19-Jasper-Java8-tc6-v1.patch Modified: tomcat/tc6.0.x/trunk/conf/web.xml URL: http://svn.apache.org/viewvc/tomcat/tc6.0.x/trunk/conf/web.xml?rev=1585853&r1=1585852&r2=1585853&view=diff ============================================================================== --- tomcat/tc6.0.x/trunk/conf/web.xml (original) +++ tomcat/tc6.0.x/trunk/conf/web.xml Tue Apr 8 22:19:46 2014 @@ -87,10 +87,12 @@ <!-- globalXsltFile[null] --> <!-- --> <!-- globalXsltFile Site wide configuration version of --> - <!-- localXsltFile This argument is expected --> - <!-- to be a physical file. [null] --> - <!-- --> - <!-- --> + <!-- localXsltFile. This argument must either be an --> + <!-- absolute or relative (to either --> + <!-- $CATALINA_BASE/conf or $CATALINA_HOME/conf) --> + <!-- path that points to a location below either --> + <!-- $CATALINA_BASE/conf (checked first) or --> + <!-- $CATALINA_HOME/conf (checked second).[null] --> <servlet> <servlet-name>default</servlet-name> Modified: tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/DefaultServlet.java URL: http://svn.apache.org/viewvc/tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/DefaultServlet.java?rev=1585853&r1=1585852&r2=1585853&view=diff ============================================================================== --- tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/DefaultServlet.java (original) +++ tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/DefaultServlet.java Tue Apr 8 22:19:46 2014 @@ -14,8 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package org.apache.catalina.servlets; @@ -35,6 +33,7 @@ import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.Iterator; +import java.util.Locale; import java.util.StringTokenizer; import javax.naming.InitialContext; @@ -48,10 +47,14 @@ import javax.servlet.UnavailableExceptio import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Source; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; @@ -65,6 +68,10 @@ import org.apache.naming.resources.Cache import org.apache.naming.resources.ProxyDirContext; import org.apache.naming.resources.Resource; import org.apache.naming.resources.ResourceAttributes; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.ext.EntityResolver2; /** @@ -104,7 +111,7 @@ import org.apache.naming.resources.Resou * </pre> * <p> * Then a request to <code>/context/static/images/tomcat.jpg</code> will succeed - * while a request to <code>/context/images/tomcat2.jpg</code> will fail. + * while a request to <code>/context/images/tomcat2.jpg</code> will fail. * </p> * @author Craig R. McClanahan * @author Remy Maucherat @@ -113,9 +120,14 @@ import org.apache.naming.resources.Resou public class DefaultServlet extends HttpServlet { - - // ----------------------------------------------------- Instance Variables + private static final DocumentBuilderFactory factory; + + private static final SecureEntityResolver secureEntityResolver = + new SecureEntityResolver(); + + + // ----------------------------------------------------- Instance Variables /** * The debugging detail level for this servlet. @@ -163,8 +175,8 @@ public class DefaultServlet * Allow customized directory listing per context. */ protected String contextXsltFile = null; - - + + /** * Allow customized directory listing per instance. */ @@ -188,13 +200,13 @@ public class DefaultServlet * the platform default is used. */ protected String fileEncoding = null; - - + + /** * Minimum size for sendfile usage in bytes. */ protected int sendfileSize = 48 * 1024; - + /** * Should the Accept-Ranges: bytes header be send with static resources? */ @@ -204,8 +216,8 @@ public class DefaultServlet * Full range marker. */ protected static ArrayList FULL = new ArrayList(); - - + + // ----------------------------------------------------- Static Initializer @@ -219,6 +231,10 @@ public class DefaultServlet urlEncoder.addSafeCharacter('.'); urlEncoder.addSafeCharacter('*'); urlEncoder.addSafeCharacter('/'); + + factory = DocumentBuilderFactory.newInstance(); + factory.setNamespaceAware(true); + factory.setValidating(false); } @@ -277,7 +293,7 @@ public class DefaultServlet readOnly = Boolean.parseBoolean(getServletConfig().getInitParameter("readonly")); if (getServletConfig().getInitParameter("sendfileSize") != null) - sendfileSize = + sendfileSize = Integer.parseInt(getServletConfig().getInitParameter("sendfileSize")) * 1024; fileEncoding = getServletConfig().getInitParameter("fileEncoding"); @@ -371,7 +387,7 @@ public class DefaultServlet /** * Determines the appropriate path to prepend resources with - * when generating directory listings. Depending on the behaviour of + * when generating directory listings. Depending on the behaviour of * {@link #getRelativePath(HttpServletRequest)} this will change. * @param request the request to determine the path for * @return the prefix to apply to all resources in the listing. @@ -429,7 +445,7 @@ public class DefaultServlet * * @param resp the {@link HttpServletResponse} object that * contains the response the servlet returns - * to the client + * to the client * * @exception IOException if an input or output error occurs * while the servlet is handling the @@ -457,11 +473,11 @@ public class DefaultServlet } // Always allow options allow.append(", OPTIONS"); - + resp.setHeader("Allow", allow.toString()); } - - + + /** * Process a POST request for the specified resource. * @@ -741,7 +757,7 @@ public class DefaultServlet CacheEntry cacheEntry = resources.lookupCache(path); if (!cacheEntry.exists) { - // Check if we're included so we can return the appropriate + // Check if we're included so we can return the appropriate // missing resource name in the error String requestUri = (String) request.getAttribute( Globals.INCLUDE_REQUEST_URI_ATTR); @@ -766,7 +782,7 @@ public class DefaultServlet // ends with "/" or "\", return NOT FOUND if (cacheEntry.context == null) { if (path.endsWith("/") || (path.endsWith("\\"))) { - // Check if we're included so we can return the appropriate + // Check if we're included so we can return the appropriate // missing resource name in the error String requestUri = (String) request.getAttribute( Globals.INCLUDE_REQUEST_URI_ATTR); @@ -827,13 +843,13 @@ public class DefaultServlet // Accept ranges header response.setHeader("Accept-Ranges", "bytes"); } - + // Parse range specifier ranges = parseRange(request, response, cacheEntry.attributes); - + // ETag header response.setHeader("ETag", cacheEntry.attributes.getETag()); - + // Last-Modified header response.setHeader("Last-Modified", cacheEntry.attributes.getLastModifiedHttp()); @@ -1194,24 +1210,22 @@ public class DefaultServlet } - /** * Decide which way to render. HTML or XML. */ protected InputStream render(String contextPath, CacheEntry cacheEntry) throws IOException, ServletException { - InputStream xsltInputStream = - findXsltInputStream(cacheEntry.context); + Source xsltSource = findXsltInputStream(cacheEntry.context); - if (xsltInputStream==null) { + if (xsltSource == null) { return renderHtml(contextPath, cacheEntry); - } else { - return renderXml(contextPath, cacheEntry, xsltInputStream); } + return renderXml(contextPath, cacheEntry, xsltSource); } + /** * Return an InputStream to an HTML representation of the contents * of this directory. @@ -1221,7 +1235,7 @@ public class DefaultServlet */ protected InputStream renderXml(String contextPath, CacheEntry cacheEntry, - InputStream xsltInputStream) + Source xsltSource) throws IOException, ServletException { StringBuilder sb = new StringBuilder(); @@ -1243,7 +1257,7 @@ public class DefaultServlet // Render the directory entries within this directory NamingEnumeration enumeration = resources.list(cacheEntry.name); - + // rewriteUrl(contextPath) is expensive. cache result for later reuse String rewrittenContextPath = rewriteUrl(contextPath); @@ -1314,8 +1328,7 @@ public class DefaultServlet try { TransformerFactory tFactory = TransformerFactory.newInstance(); Source xmlSource = new StreamSource(new StringReader(sb.toString())); - Source xslSource = new StreamSource(xsltInputStream); - Transformer transformer = tFactory.newTransformer(xslSource); + Transformer transformer = tFactory.newTransformer(xsltSource); ByteArrayOutputStream stream = new ByteArrayOutputStream(); OutputStreamWriter osWriter = new OutputStreamWriter(stream, "UTF8"); @@ -1353,7 +1366,7 @@ public class DefaultServlet PrintWriter writer = new PrintWriter(osWriter); StringBuilder sb = new StringBuilder(); - + // rewriteUrl(contextPath) is expensive. cache result for later reuse String rewrittenContextPath = rewriteUrl(contextPath); @@ -1540,9 +1553,9 @@ public class DefaultServlet /** - * Return the xsl template inputstream (if possible) + * Return a Source for the xsl template (if possible) */ - protected InputStream findXsltInputStream(DirContext directory) + protected Source findXsltInputStream(DirContext directory) throws IOException, ServletException { if (localXsltFile != null) { @@ -1550,8 +1563,13 @@ public class DefaultServlet Object obj = directory.lookup(localXsltFile); if ((obj != null) && (obj instanceof Resource)) { InputStream is = ((Resource) obj).streamContent(); - if (is != null) - return is; + if (is != null) { + if (Globals.IS_SECURITY_ENABLED) { + return secureXslt(is); + } else { + return new StreamSource(is); + } + } } } catch (NamingException e) { if (debug > 10) @@ -1562,8 +1580,13 @@ public class DefaultServlet if (contextXsltFile != null) { InputStream is = getServletContext().getResourceAsStream(contextXsltFile); - if (is != null) - return is; + if (is != null) { + if (Globals.IS_SECURITY_ENABLED) { + return secureXslt(is); + } else { + return new StreamSource(is); + } + } if (debug > 10) log("contextXsltFile '" + contextXsltFile + "' not found"); @@ -1572,20 +1595,26 @@ public class DefaultServlet /* Open and read in file in one fell swoop to reduce chance * chance of leaving handle open. */ - if (globalXsltFile!=null) { - FileInputStream fis = null; - - try { - File f = new File(globalXsltFile); - if (f.exists()){ - fis =new FileInputStream(f); + if (globalXsltFile != null) { + File f = validateGlobalXsltFile(); + if (f != null){ + FileInputStream fis = null; + try { + fis = new FileInputStream(f); byte b[] = new byte[(int)f.length()]; /* danger! */ fis.read(b); - return new ByteArrayInputStream(b); + return new StreamSource(new ByteArrayInputStream(b)); + } finally { + if (fis != null) { + try { + fis.close(); + } catch (IOException ioe) { + if (debug > 10) { + log(ioe.getMessage(), ioe); + } + } + } } - } finally { - if (fis!=null) - fis.close(); } } @@ -1594,9 +1623,94 @@ public class DefaultServlet } - // -------------------------------------------------------- protected Methods + private File validateGlobalXsltFile() { + + File result = null; + String base = System.getProperty("catalina.base"); + + if (base != null) { + File baseConf = new File(base, "conf"); + result = validateGlobalXsltFile(baseConf); + } + + if (result == null) { + String home = System.getProperty("catalina.home"); + if (home != null && !home.equals(base)) { + File homeConf = new File(home, "conf"); + result = validateGlobalXsltFile(homeConf); + } + } + + return result; + } + + + private File validateGlobalXsltFile(File base) { + File candidate = new File(globalXsltFile); + if (!candidate.isAbsolute()) { + candidate = new File(base, globalXsltFile); + } + + if (!candidate.isFile()) { + return null; + } + + // First check that the resulting path is under the provided base + try { + if (!candidate.getCanonicalPath().startsWith(base.getCanonicalPath())) { + return null; + } + } catch (IOException ioe) { + return null; + } + + // Next check that an .xsl or .xslt file has been specified + String nameLower = candidate.getName().toLowerCase(Locale.ENGLISH); + if (!nameLower.endsWith(".xslt") && !nameLower.endsWith(".xsl")) { + return null; + } + + return candidate; + } + + + private Source secureXslt(InputStream is) { + // Need to filter out any external entities + Source result = null; + try { + DocumentBuilder builder = factory.newDocumentBuilder(); + builder.setEntityResolver(secureEntityResolver); + Document document = builder.parse(is); + result = new DOMSource(document); + } catch (ParserConfigurationException e) { + if (debug > 0) { + log(e.getMessage(), e); + } + } catch (SAXException e) { + if (debug > 0) { + log(e.getMessage(), e); + } + } catch (IOException e) { + if (debug > 0) { + log(e.getMessage(), e); + } + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + if (debug > 10) { + log(e.getMessage(), e); + } + } + } + } + return result; + } + // -------------------------------------------------------- protected Methods + /** * Check if sendfile can be used. */ @@ -1624,8 +1738,8 @@ public class DefaultServlet return false; } } - - + + /** * Check if the if-match condition is satisfied. * @@ -2034,7 +2148,7 @@ public class DefaultServlet while ( (exception == null) && (ranges.hasNext()) ) { InputStream resourceInputStream = cacheEntry.resource.streamContent(); - + Reader reader; if (fileEncoding == null) { reader = new InputStreamReader(resourceInputStream); @@ -2238,10 +2352,6 @@ public class DefaultServlet } - - // ------------------------------------------------------ Range Inner Class - - protected class Range { public long start; @@ -2267,4 +2377,29 @@ public class DefaultServlet } + /** + * This is secure in the sense that any attempt to use an external entity + * will trigger an exception. + */ + private static class SecureEntityResolver implements EntityResolver2 { + + public InputSource resolveEntity(String publicId, String systemId) + throws SAXException, IOException { + throw new SAXException(sm.getString("defaultServlet.blockExternalEntity", + publicId, systemId)); + } + + public InputSource getExternalSubset(String name, String baseURI) + throws SAXException, IOException { + throw new SAXException(sm.getString("defaultServlet.blockExternalSubset", + name, baseURI)); + } + + public InputSource resolveEntity(String name, String publicId, + String baseURI, String systemId) throws SAXException, + IOException { + throw new SAXException(sm.getString("defaultServlet.blockExternalEntity2", + name, publicId, baseURI, systemId)); + } + } } Modified: tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/LocalStrings.properties?rev=1585853&r1=1585852&r2=1585853&view=diff ============================================================================== --- tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/LocalStrings.properties (original) +++ tomcat/tc6.0.x/trunk/java/org/apache/catalina/servlets/LocalStrings.properties Tue Apr 8 22:19:46 2014 @@ -13,6 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +defaultServlet.blockExternalEntity=Blocked access to external entity with publicId [{0}] and systemId [{0}] +defaultServlet.blockExternalEntity2=Blocked access to external entity with name [{0}], publicId [{1}], baseURI [{2}] and systemId [{3}] +defaultServlet.blockExternalSubset=Blocked access to external subset with name [{0}] and baseURI [{1}] defaultServlet.missingResource=The requested resource ({0}) is not available defaultservlet.directorylistingfor=Directory Listing for: defaultservlet.upto=Up to: Modified: tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml?rev=1585853&r1=1585852&r2=1585853&view=diff ============================================================================== --- tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml (original) +++ tomcat/tc6.0.x/trunk/webapps/docs/changelog.xml Tue Apr 8 22:19:46 2014 @@ -71,6 +71,12 @@ filters throw exceptions when their destroy() method is called. (markt/kkolinko) </fix> + <fix> + Redefine the <code>globalXsltFile</code> initialisation parameter of the + DefaultServlet as relative to CATALINA_BASE/conf or CATALINA_HOME/conf. + Prevent user supplied XSLTs used by the DefaultServlet from defining + external entities. (markt) + </fix> </changelog> </subsection> <subsection name="Coyote"> Modified: tomcat/tc6.0.x/trunk/webapps/docs/default-servlet.xml URL: http://svn.apache.org/viewvc/tomcat/tc6.0.x/trunk/webapps/docs/default-servlet.xml?rev=1585853&r1=1585852&r2=1585853&view=diff ============================================================================== --- tomcat/tc6.0.x/trunk/webapps/docs/default-servlet.xml (original) +++ tomcat/tc6.0.x/trunk/webapps/docs/default-servlet.xml Tue Apr 8 22:19:46 2014 @@ -110,21 +110,23 @@ The DefaultServlet allows the following <th valign='top'>globalXsltFile</th> <td valign='top'> If you wish to customize your directory listing, you - can use an XSL transformation. This value is an absolute - file name which be used for all directory listings. - This can be overridden per context and/or per directory. See - <strong>contextXsltFile</strong> and <strong>localXsltFile</strong> - below. The format of the xml is shown below. + can use an XSL transformation. This value is a relative file name (to + either $CATALINA_BASE/conf/ or $CATALINA_HOME/conf/) which will be used + for all directory listings. This can be overridden per context and/or + per directory. See <strong>contextXsltFile</strong> and + <strong>localXsltFile</strong> below. The format of the xml is shown + below. </td> </tr> <tr> <th valign='top'>contextXsltFile</th> <td valign='top'> You may also customize your directory listing by context by - configuring <code>contextXsltFile</code>. This should be a context - relative path (e.g.: <code>/path/to/context.xslt</code>). This - overrides <code>globalXsltFile</code>. If this value is present but a - file does not exist, then <code>globalXsltFile</code> will be used. If + configuring <code>contextXsltFile</code>. This must be a context + relative path (e.g.: <code>/path/to/context.xslt</code>) to a file with + a <code>.xsl</code> or <code>.xslt</code> extension. This overrides + <code>globalXsltFile</code>. If this value is present but a file does + not exist, then <code>globalXsltFile</code> will be used. If <code>globalXsltFile</code> does not exist, then the default directory listing will be shown. </td> @@ -133,11 +135,12 @@ The DefaultServlet allows the following <th valign='top'>localXsltFile</th> <td valign='top'> You may also customize your directory listing by directory by - configuring <code>localXsltFile</code>. This should be a relative - file name in the directory where the listing will take place. - This overrides <code>globalXsltFile</code> and - <code>contextXsltFile</code>. If this value is present but a file - does not exist, then <code>contextXsltFile</code> will be used. If + configuring <code>localXsltFile</code>. This must be a file in the + directory where the listing will take place to with a + <code>.xsl</code> or <code>.xslt</code> extension. This overrides + <code>globalXsltFile</code> and <code>contextXsltFile</code>. If this + value is present but a file does not exist, then + <code>contextXsltFile</code> will be used. If <code>contextXsltFile</code> does not exist, then <code>globalXsltFile</code> will be used. If <code>globalXsltFile</code> does not exist, then the default --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org