You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by ma...@apache.org on 2019/07/24 14:51:30 UTC

[tomcat] 10/11: Fix https://bz.apache.org/bugzilla/show_bug.cgi?id=63578

This is an automated email from the ASF dual-hosted git repository.

markt pushed a commit to branch 7.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 3de983433578765d52179e4b1c69ad04019673b2
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Tue Jul 23 19:07:19 2019 +0100

    Fix https://bz.apache.org/bugzilla/show_bug.cgi?id=63578
    
    Various fixes to return 400 responses rather than 500 responses when the
    provided request is invalid.
---
 .../apache/catalina/connector/CoyoteAdapter.java   | 11 +++-
 .../coyote/http11/AbstractHttp11Processor.java     | 46 ++++++++++------
 .../apache/coyote/http11/LocalStrings.properties   |  4 +-
 .../connector/TestCoyoteAdapterRequestFuzzing.java | 63 +++++++++++++++++-----
 .../apache/catalina/startup/SimpleHttpClient.java  | 12 ++++-
 test/org/apache/tomcat/unittest/TesterData.java    | 37 +++++++++++++
 webapps/docs/changelog.xml                         |  8 +++
 7 files changed, 149 insertions(+), 32 deletions(-)

diff --git a/java/org/apache/catalina/connector/CoyoteAdapter.java b/java/org/apache/catalina/connector/CoyoteAdapter.java
index 151fb48..3813073 100644
--- a/java/org/apache/catalina/connector/CoyoteAdapter.java
+++ b/java/org/apache/catalina/connector/CoyoteAdapter.java
@@ -816,7 +816,16 @@ public class CoyoteAdapter implements Adapter {
             }
 
             // Look for session ID in cookies and SSL session
-            parseSessionCookiesId(req, request);
+            try {
+                parseSessionCookiesId(req, request);
+            } catch (IllegalArgumentException e) {
+                // Too many cookies
+                if (!response.isError()) {
+                    response.setError();
+                    response.sendError(400);
+                }
+                return false;
+            }
             parseSessionSslId(request);
 
             sessionID = request.getRequestedSessionId();
diff --git a/java/org/apache/coyote/http11/AbstractHttp11Processor.java b/java/org/apache/coyote/http11/AbstractHttp11Processor.java
index ae0bb2f..1bd0adb 100644
--- a/java/org/apache/coyote/http11/AbstractHttp11Processor.java
+++ b/java/org/apache/coyote/http11/AbstractHttp11Processor.java
@@ -1313,7 +1313,7 @@ public abstract class AbstractHttp11Processor<S> extends AbstractProcessor<S> {
 
         // Check connection header
         MessageBytes connectionValueMB = headers.getValue(Constants.CONNECTION);
-        if (connectionValueMB != null) {
+        if (connectionValueMB != null && !connectionValueMB.isNull()) {
             ByteChunk connectionValueBC = connectionValueMB.getByteChunk();
             if (findBytes(connectionValueBC, Constants.CLOSE_BYTES) != -1) {
                 keepAlive = false;
@@ -1323,17 +1323,16 @@ public abstract class AbstractHttp11Processor<S> extends AbstractProcessor<S> {
             }
         }
 
-        MessageBytes expectMB = null;
         if (http11) {
-            expectMB = headers.getValue("expect");
-        }
-        if (expectMB != null) {
-            if (expectMB.indexOfIgnoreCase("100-continue", 0) != -1) {
-                getInputBuffer().setSwallowInput(false);
-                expectation = true;
-            } else {
-                response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED);
-                setErrorState(ErrorState.CLOSE_CLEAN, null);
+            MessageBytes expectMB = headers.getValue("expect");
+            if (expectMB != null && !expectMB.isNull()) {
+                if (expectMB.indexOfIgnoreCase("100-continue", 0) != -1) {
+                    getInputBuffer().setSwallowInput(false);
+                    expectation = true;
+                } else {
+                    response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED);
+                    setErrorState(ErrorState.CLOSE_CLEAN, null);
+                }
             }
         }
 
@@ -1342,7 +1341,7 @@ public abstract class AbstractHttp11Processor<S> extends AbstractProcessor<S> {
             MessageBytes userAgentValueMB = headers.getValue("user-agent");
             // Check in the restricted list, and adjust the http11
             // and keepAlive flags accordingly
-            if(userAgentValueMB != null) {
+            if(userAgentValueMB != null && !userAgentValueMB.isNull()) {
                 String userAgentValue = userAgentValueMB.toString();
                 if (restrictedUserAgents.matcher(userAgentValue).matches()) {
                     http11 = false;
@@ -1442,8 +1441,16 @@ public abstract class AbstractHttp11Processor<S> extends AbstractProcessor<S> {
                 } else {
                     // Not HTTP/1.1 - no Host header so generate one since
                     // Tomcat internals assume it is set
-                    hostValueMB = headers.setValue("host");
-                    hostValueMB.setBytes(uriB, uriBCStart + pos, slashPos - pos);
+                    try {
+                        hostValueMB = headers.setValue("host");
+                        hostValueMB.setBytes(uriB, uriBCStart + pos, slashPos - pos);
+                    } catch (IllegalStateException e) {
+                        // Edge case
+                        // If the request has too many headers it won't be
+                        // possible to create the host header. Ignore this as
+                        // processing won't reach the point where the Tomcat
+                        // internals expect there to be a host header.
+                    }
                 }
             } else {
                 badRequest("http11processor.request.invalidScheme");
@@ -1467,7 +1474,7 @@ public abstract class AbstractHttp11Processor<S> extends AbstractProcessor<S> {
         if (http11) {
             transferEncodingValueMB = headers.getValue("transfer-encoding");
         }
-        if (transferEncodingValueMB != null) {
+        if (transferEncodingValueMB != null && !transferEncodingValueMB.isNull()) {
             String transferEncodingValue = transferEncodingValueMB.toString();
             // Parse the comma separated list. "identity" codings are ignored
             int startPos = 0;
@@ -1484,7 +1491,14 @@ public abstract class AbstractHttp11Processor<S> extends AbstractProcessor<S> {
         }
 
         // Parse content-length header
-        long contentLength = request.getContentLengthLong();
+        long contentLength = -1;
+        try {
+            contentLength = request.getContentLengthLong();
+        } catch (NumberFormatException e) {
+            badRequest("http11processor.request.nonNumericContentLength");
+        } catch (IllegalArgumentException e) {
+            badRequest("http11processor.request.multipleContentLength");
+        }
         if (contentLength >= 0) {
             if (contentDelimitation) {
                 // contentDelimitation being true at this point indicates that
diff --git a/java/org/apache/coyote/http11/LocalStrings.properties b/java/org/apache/coyote/http11/LocalStrings.properties
index e41e9d2..035bf71 100644
--- a/java/org/apache/coyote/http11/LocalStrings.properties
+++ b/java/org/apache/coyote/http11/LocalStrings.properties
@@ -26,10 +26,12 @@ http11processor.regexp.error=Error parsing regular expression {0}
 http11processor.request.finish=Error finishing request
 http11processor.request.inconsistentHosts=The host specified in the request line is not consistent with the host header
 http11processor.request.invalidScheme=The HTTP request contained an absolute URI with an invalid scheme
-http11processor.request.invalidUri==The HTTP request contained an invalid URI
+http11processor.request.invalidUri=The HTTP request contained an invalid URI
 http11processor.request.invalidUserInfo=The HTTP request contained an absolute URI with an invalid userinfo
+http11processor.request.multipleContentLength=The request contained multiple content-length headers
 http11processor.request.multipleHosts=The request contained multiple host headers
 http11processor.request.noHostHeader=The HTTP/1.1 request did not provide a host header
+http11processor.request.nonNumericContentLength=The request contained a content-length header with a non-numeric value
 http11processor.request.prepare=Error preparing request
 http11processor.request.process=Error processing request
 http11processor.response.finish=Error finishing response
diff --git a/test/org/apache/catalina/connector/TestCoyoteAdapterRequestFuzzing.java b/test/org/apache/catalina/connector/TestCoyoteAdapterRequestFuzzing.java
index 453e04a..a34adc5 100644
--- a/test/org/apache/catalina/connector/TestCoyoteAdapterRequestFuzzing.java
+++ b/test/org/apache/catalina/connector/TestCoyoteAdapterRequestFuzzing.java
@@ -27,11 +27,13 @@ import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameter;
 
+import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
 import org.apache.catalina.Context;
 import org.apache.catalina.servlets.DefaultServlet;
 import org.apache.catalina.startup.SimpleHttpClient;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.tomcat.unittest.TesterData;
 
 /*
  * Various requests, usually originating from fuzzing, that have triggered an
@@ -41,21 +43,61 @@ import org.apache.catalina.startup.TomcatBaseTest;
 @RunWith(Parameterized.class)
 public class TestCoyoteAdapterRequestFuzzing extends TomcatBaseTest {
 
-    @Parameterized.Parameters(name = "{index}: uri[{0}], host[{1}], expected[{2}]")
+    private static final String VALUE_16K = TesterData.string('x', 16 * 1024);
+    // Default max header count is 100
+    private static final String HEADER_150 = TesterData.string("X-Tomcat-Test: a" + CRLF, 150);
+    // Default max header count is 200 (need to keep under maxHeaderCount as well)
+    private static final String COOKIE_250 = TesterData.string("Cookie: a=b;c=d;e=f;g=h" + CRLF, 75);
+
+    @Parameterized.Parameters(name = "{index}: requestline[{0}], expected[{2}]")
     public static Collection<Object[]> parameters() {
         List<Object[]> parameterSets = new ArrayList<Object[]>();
 
-        parameterSets.add(new Object[] { "/", "lÿ#", "400" } );
-        parameterSets.add(new Object[] { "*;", "", "400" } );
+        parameterSets.add(new Object[] { "GET /00 HTTP/1.1",
+                                         "Host: lÿ#" + CRLF,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET *; HTTP/1.1",
+                                         "Host: localhost" + CRLF,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET /02 HTTP/1.1",
+                                         "Host: localhost" + CRLF +
+                                         "Content-Length: \u00A0" + CRLF,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET /03 HTTP/1.1",
+                                         "Content-Length: 1" + CRLF +
+                                         "Content-Length: 1" + CRLF,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET /04 HTTP/1.1",
+                                         "Transfer-Encoding: " + VALUE_16K + CRLF,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET /05 HTTP/1.1",
+                                         "Expect: " + VALUE_16K + CRLF,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET /06 HTTP/1.1",
+                                         "Connection: " + VALUE_16K + CRLF,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET /07 HTTP/1.1",
+                                         "User-Agent: " + VALUE_16K + CRLF,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET /08 HTTP/1.1",
+                                         HEADER_150,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET http://host/09 HTTP/1.0",
+                                         HEADER_150,
+                                         "400" } );
+        parameterSets.add(new Object[] { "GET /10 HTTP/1.1",
+                                         "Host: localhost" + CRLF +
+                                         COOKIE_250,
+                                         "400" } );
 
         return parameterSets;
     }
 
     @Parameter(0)
-    public String uri;
+    public String requestLine;
 
     @Parameter(1)
-    public String host;
+    public String headers;
 
     @Parameter(2)
     public String expected;
@@ -64,6 +106,7 @@ public class TestCoyoteAdapterRequestFuzzing extends TomcatBaseTest {
     @Test
     public void doTest() throws Exception {
         Tomcat tomcat = getTomcatInstance();
+        tomcat.getConnector().setAttribute("restrictedUserAgents", "value-not-important");
 
         File appDir = new File("test/webapp");
         Context ctxt = tomcat.addContext("", appDir.getAbsolutePath());
@@ -72,20 +115,15 @@ public class TestCoyoteAdapterRequestFuzzing extends TomcatBaseTest {
 
         tomcat.start();
 
-        String request =
-                "GET " + uri + " HTTP/1.1" + SimpleHttpClient.CRLF +
-                "Host: " + host + SimpleHttpClient.CRLF +
-                 SimpleHttpClient.CRLF;
-
         Client client = new Client(tomcat.getConnector().getLocalPort());
-        client.setRequest(new String[] {request});
+        client.setRequest(new String[] {requestLine + CRLF, headers + CRLF});
 
         client.connect();
         client.processRequest();
 
         // Expected response
         String line = client.getResponseLine();
-        Assert.assertTrue(line, line.startsWith("HTTP/1.1 " + expected + " "));
+        Assert.assertTrue(line + CRLF + client.getResponseBody(), line.startsWith("HTTP/1.1 " + expected + " "));
     }
 
 
@@ -93,6 +131,7 @@ public class TestCoyoteAdapterRequestFuzzing extends TomcatBaseTest {
 
         public Client(int port) {
             setPort(port);
+            setRequestPause(0);
         }
 
         @Override
diff --git a/test/org/apache/catalina/startup/SimpleHttpClient.java b/test/org/apache/catalina/startup/SimpleHttpClient.java
index 7e33c76..dd02566 100644
--- a/test/org/apache/catalina/startup/SimpleHttpClient.java
+++ b/test/org/apache/catalina/startup/SimpleHttpClient.java
@@ -28,6 +28,7 @@ import java.io.Writer;
 import java.net.InetSocketAddress;
 import java.net.Socket;
 import java.net.SocketAddress;
+import java.net.SocketException;
 import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.List;
@@ -305,8 +306,15 @@ public abstract class SimpleHttpClient {
             else {
                 // not using content length, so just read it line by line
                 String line = null;
-                while ((line = readLine()) != null) {
-                    builder.append(line);
+                try {
+                    while ((line = readLine()) != null) {
+                        builder.append(line);
+                    }
+                } catch (SocketException e) {
+                    // Ignore
+                    // May see a SocketException if the request hasn't been
+                    // fully read when the connection is closed as that may
+                    // trigger a TCP reset.
                 }
             }
         }
diff --git a/test/org/apache/tomcat/unittest/TesterData.java b/test/org/apache/tomcat/unittest/TesterData.java
new file mode 100644
index 0000000..fbdc72a
--- /dev/null
+++ b/test/org/apache/tomcat/unittest/TesterData.java
@@ -0,0 +1,37 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You 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.apache.tomcat.unittest;
+
+public class TesterData {
+
+    public static String string(char c, int count) {
+        StringBuilder sb = new StringBuilder(count);
+        for (int i = 0; i < count; i++) {
+            sb.append(c);
+        }
+        return sb.toString();
+    }
+
+
+    public static String string(String str, int count) {
+        StringBuilder sb = new StringBuilder(str.length() * count);
+        for (int i = 0; i < count; i++) {
+            sb.append(str);
+        }
+        return sb.toString();
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 948b825..a262706 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -60,6 +60,14 @@
   issues do not "pop up" wrt. others).
 -->
 <section name="Tomcat 7.0.97 (violetagg)">
+  <subsection name="Coyote">
+    <changelog>
+      <fix>
+        <bug>63578</bug>: Improve handling of invalid requests so that 400
+        responses are returned to the client rather than 500 responses. (markt)
+      </fix>
+    </changelog>
+  </subsection>
 </section>
 <section name="Tomcat 7.0.96 (violetagg)">
   <subsection name="Catalina">


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org