Skip to content

Commit

Permalink
Move the functionality that provides redirects for context roots and …
Browse files Browse the repository at this point in the history
…directories where a trailing <code>/</code> is added from the Mapper to the DefaultServlet. This enables such requests to be processed by any configured Valves and Filters before the redirect is made. This behaviour is configurable via the mapperContextRootRedirectEnabled and mapperDirectoryRedirectEnabled attributes of the Context which may be used to restore the previous behaviour.

This is part 1 of 3 of the fix for CVE-2015-5345

git-svn-id: https://svn.apache.org/repos/asf/tomcat/trunk@1715206 13f79535-47bb-0310-9956-ffa450edef68
  • Loading branch information
markt-asf committed Nov 19, 2015
1 parent 9cc01c8 commit 8437193
Show file tree
Hide file tree
Showing 12 changed files with 232 additions and 8 deletions.
40 changes: 40 additions & 0 deletions java/org/apache/catalina/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -1714,4 +1714,44 @@ Set<String> addServletSecurity(ServletRegistration.Dynamic registration,
* false}
*/
public boolean getValidateClientProvidedNewSessionId();

/**
* If enabled, requests for a web application context root will be
* redirected (adding a trailing slash) by the Mapper. This is more
* efficient but has the side effect of confirming that the context path is
* valid.
*
* @param mapperContextRootRedirectEnabled Should the redirects be enabled?
*/
public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled);

/**
* Determines if requests for a web application context root will be
* redirected (adding a trailing slash) by the Mapper. This is more
* efficient but has the side effect of confirming that the context path is
* valid.
*
* @return {@code true} if the Mapper level redirect is enabled for this
* Context.
*/
public boolean getMapperContextRootRedirectEnabled();

/**
* If enabled, requests for a directory will be redirected (adding a
* trailing slash) by the Mapper. This is more efficient but has the
* side effect of confirming that the directory is valid.
*
* @param mapperDirectoryRedirectEnabled Should the redirects be enabled?
*/
public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled);

/**
* Determines if requests for a directory will be redirected (adding a
* trailing slash) by the Mapper. This is more efficient but has the
* side effect of confirming that the directory is valid.
*
* @return {@code true} if the Mapper level redirect is enabled for this
* Context.
*/
public boolean getMapperDirectoryRedirectEnabled();
}
41 changes: 41 additions & 0 deletions java/org/apache/catalina/core/StandardContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -816,13 +816,53 @@ public void unbind() {}

private boolean validateClientProvidedNewSessionId = true;

boolean mapperContextRootRedirectEnabled = false;

boolean mapperDirectoryRedirectEnabled = false;


// ----------------------------------------------------- Context Properties

@Override
public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled) {
this.mapperContextRootRedirectEnabled = mapperContextRootRedirectEnabled;
}


/**
* {@inheritDoc}
* <p>
* The default value for this implementation is {@code false}.
*/
@Override
public boolean getMapperContextRootRedirectEnabled() {
return mapperContextRootRedirectEnabled;
}


@Override
public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled) {
this.mapperDirectoryRedirectEnabled = mapperDirectoryRedirectEnabled;
}


/**
* {@inheritDoc}
* <p>
* The default value for this implementation is {@code false}.
*/
@Override
public boolean getMapperDirectoryRedirectEnabled() {
return mapperDirectoryRedirectEnabled;
}


@Override
public void setValidateClientProvidedNewSessionId(boolean validateClientProvidedNewSessionId) {
this.validateClientProvidedNewSessionId = validateClientProvidedNewSessionId;
}


/**
* {@inheritDoc}
* <p>
Expand All @@ -833,6 +873,7 @@ public boolean getValidateClientProvidedNewSessionId() {
return validateClientProvidedNewSessionId;
}


@Override
public void setCookieProcessor(CookieProcessor cookieProcessor) {
if (cookieProcessor == null) {
Expand Down
8 changes: 8 additions & 0 deletions java/org/apache/catalina/core/mbeans-descriptors.xml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,14 @@
description="Associated manager."
type="org.apache.catalina.Manager" />

<attribute name="mapperContextRootRedirectEnabled"
description="Should the Mapper be used for context root redirects"
type="boolean" />

<attribute name="mapperDirectoryRedirectEnabled"
description="Should the Mapper be used for directory redirects"
type="boolean" />

<attribute name="namingContextListener"
description="Associated naming context listener."
type="org.apache.catalina.core.NamingContextListener" />
Expand Down
10 changes: 5 additions & 5 deletions java/org/apache/catalina/mapper/Mapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,8 @@ private final void internalMapWrapper(ContextVersion contextVersion,
}
}

if(mappingData.wrapper == null && noServletPath) {
if(mappingData.wrapper == null && noServletPath &&
mappingData.context.getMapperContextRootRedirectEnabled()) {
// The path is empty, redirect to "/"
mappingData.redirectPath.setChars
(path.getBuffer(), pathOffset, pathEnd-pathOffset);
Expand Down Expand Up @@ -1001,9 +1002,9 @@ private final void internalMapWrapper(ContextVersion contextVersion,
char[] buf = path.getBuffer();
if (contextVersion.resources != null && buf[pathEnd -1 ] != '/') {
String pathStr = path.toString();
WebResource file =
contextVersion.resources.getResource(pathStr);
if (file != null && file.isDirectory()) {
WebResource file = contextVersion.resources.getResource(pathStr);
if (file != null && file.isDirectory() &&
mappingData.context.getMapperDirectoryRedirectEnabled()) {
// Note: this mutates the path: do not do any processing
// after this (since we set the redirectPath, there
// shouldn't be any)
Expand All @@ -1020,7 +1021,6 @@ private final void internalMapWrapper(ContextVersion contextVersion,

path.setOffset(pathOffset);
path.setEnd(pathEnd);

}


Expand Down
11 changes: 11 additions & 0 deletions java/org/apache/catalina/servlets/DefaultServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,17 @@ protected void serveResource(HttpServletRequest request,
long contentLength = -1L;

if (resource.isDirectory()) {
if (!path.endsWith("/")) {
StringBuilder location = new StringBuilder(request.getRequestURI());
location.append('/');
if (request.getQueryString() != null) {
location.append('?');
location.append(request.getQueryString());
}
response.sendRedirect(response.encodeRedirectURL(location.toString()));
return;
}

// Skip directory listings if we have been configured to
// suppress them
if (!listings) {
Expand Down
18 changes: 17 additions & 1 deletion java/org/apache/catalina/startup/FailedContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -764,9 +764,25 @@ public void setCookieProcessor(CookieProcessor cookieProcessor) { /* NO-OP */ }

@Override
public void setValidateClientProvidedNewSessionId(boolean validateClientProvidedNewSessionId) {
//NO-OP
// NO-OP
}

@Override
public boolean getValidateClientProvidedNewSessionId() { return false; }

@Override
public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled) {
// NO-OP
}

@Override
public boolean getMapperContextRootRedirectEnabled() { return false; }

@Override
public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled) {
// NO-OP
}

@Override
public boolean getMapperDirectoryRedirectEnabled() { return false; }
}
16 changes: 16 additions & 0 deletions test/org/apache/catalina/core/TesterContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -1234,4 +1234,20 @@ public void setValidateClientProvidedNewSessionId(boolean validateClientProvided

@Override
public boolean getValidateClientProvidedNewSessionId() { return false; }

@Override
public void setMapperContextRootRedirectEnabled(boolean mapperContextRootRedirectEnabled) {
// NO-OP
}

@Override
public boolean getMapperContextRootRedirectEnabled() { return false; }

@Override
public void setMapperDirectoryRedirectEnabled(boolean mapperDirectoryRedirectEnabled) {
// NO-OP
}

@Override
public boolean getMapperDirectoryRedirectEnabled() { return false; }
}
64 changes: 64 additions & 0 deletions test/org/apache/catalina/mapper/TestMapperWebapps.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.HashMap;
import java.util.List;

Expand All @@ -33,7 +34,10 @@
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.catalina.valves.RemoteAddrValve;
import org.apache.tomcat.util.buf.ByteChunk;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.apache.tomcat.websocket.server.WsContextListener;

/**
Expand Down Expand Up @@ -225,6 +229,66 @@ public void testWelcomeFileStrict() throws Exception {
Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc);
}

@Test
public void testRedirect() throws Exception {
// Disable the following of redirects for this test only
boolean originalValue = HttpURLConnection.getFollowRedirects();
HttpURLConnection.setFollowRedirects(false);
try {
Tomcat tomcat = getTomcatInstance();

// Use standard test webapp as ROOT
File rootDir = new File("test/webapp");
org.apache.catalina.Context root =
tomcat.addWebapp(null, "", rootDir.getAbsolutePath());

// Add a security constraint
SecurityConstraint constraint = new SecurityConstraint();
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/welcome-files/*");
collection.addPattern("/welcome-files");
constraint.addCollection(collection);
constraint.addAuthRole("foo");
root.addConstraint(constraint);

// Also make examples available
File examplesDir = new File(getBuildDirectory(), "webapps/examples");
org.apache.catalina.Context examples = tomcat.addWebapp(
null, "/examples", examplesDir.getAbsolutePath());
// Then block access to the examples to test redirection
RemoteAddrValve rav = new RemoteAddrValve();
rav.setDeny(".*");
rav.setDenyStatus(404);
examples.getPipeline().addValve(rav);

tomcat.start();

// Redirects within a web application
doRedirectTest("/welcome-files", 401);
doRedirectTest("/welcome-files/", 401);

doRedirectTest("/jsp", 302);
doRedirectTest("/jsp/", 404);

doRedirectTest("/WEB-INF", 404);
doRedirectTest("/WEB-INF/", 404);

// Redirects between web applications
doRedirectTest("/examples", 404);
doRedirectTest("/examples/", 404);
} finally {
HttpURLConnection.setFollowRedirects(originalValue);
}
}


private void doRedirectTest(String path, int expected) throws IOException {
ByteChunk bc = new ByteChunk();
int rc = getUrl("http://localhost:" + getPort() + path, bc, null);
Assert.assertEquals(expected, rc);
}


/**
* Prepare a string to search in messages that contain a timestamp, when it
* is known that the timestamp was printed between {@code timeA} and
Expand Down
3 changes: 1 addition & 2 deletions test/org/apache/catalina/startup/TomcatBaseTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -647,8 +647,7 @@ public static int methodUrl(String path, ByteChunk out, int readTimeout,
String method) throws IOException {

URL url = new URL(path);
HttpURLConnection connection =
(HttpURLConnection) url.openConnection();
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setUseCaches(false);
connection.setReadTimeout(readTimeout);
connection.setRequestMethod(method);
Expand Down
3 changes: 3 additions & 0 deletions test/org/apache/catalina/valves/rewrite/TestRewriteValve.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.junit.Test;

import org.apache.catalina.Context;
import org.apache.catalina.servlets.DefaultServlet;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.startup.TomcatBaseTest;
import org.apache.tomcat.util.buf.ByteChunk;
Expand Down Expand Up @@ -75,6 +76,8 @@ private void doTestRewrite(String config, String request, String expectedURI) th
Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
ctx.addServletMapping("/a/%255A", "snoop");
ctx.addServletMapping("/c/*", "snoop");
Tomcat.addServlet(ctx, "default", new DefaultServlet());
ctx.addServletMapping("/", "default");

tomcat.start();

Expand Down
10 changes: 10 additions & 0 deletions webapps/docs/changelog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@
<code>Mapper</code> used is the <code>Mapper</code> associated with the
<code>Service</code> for which the listener was created. (markt)
</scode>
<add>
Move the functionality that provides redirects for context roots and
directories where a trailing <code>/</code> is added from the Mapper to
the <code>DefaultServlet</code>. This enables such requests to be
processed by any configured Valves and Filters before the redirect is
made. This behaviour is configurable via the
<code>mapperContextRootRedirectEnabled</code> and
<code>mapperDirectoryRedirectEnabled</code> attributes of the Context
which may be used to restore the previous behaviour. (markt)
</add>
</changelog>
</subsection>
</section>
Expand Down
16 changes: 16 additions & 0 deletions webapps/docs/config/context.xml
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,22 @@
default value of <code>false</code> is used.</p>
</attribute>

<attribute name="mapperContextRootRedirectEnabled" required="false">
<p>If enabled, requests for a web application context root will be
redirected (adding a trailing slash) if necessary by the Mapper rather
than the default Servlet. This is more efficient but has the side effect
of confirming that the context path exists. If not specified, the
default value of <code>false</code> is used.</p>
</attribute>

<attribute name="mapperDirectoryRedirectEnabled" required="false">
<p>If enabled, requests for a web application directory will be
redirected (adding a trailing slash) if necessary by the Mapper rather
than the default Servlet. This is more efficient but has the side effect
of confirming that the directory is exists. If not specified, the
default value of <code>false</code> is used.</p>
</attribute>

<attribute name="override" required="false">
<p>Set to <code>true</code> to ignore any settings in both the global
or <a href="host.html">Host</a> default contexts. By default, settings
Expand Down

0 comments on commit 8437193

Please # to comment.