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

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


The following commit(s) were added to refs/heads/main by this push:
     new d909c70  Fix BZ 65785 - additional HTTP/2 header validation
d909c70 is described below

commit d909c709b639e9670edce2581293afb9626d7b5e
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Fri Jan 7 22:43:03 2022 +0000

    Fix BZ 65785 - additional HTTP/2 header validation
    
    https://bz.apache.org/bugzilla/show_bug.cgi?id=65785
---
 java/org/apache/coyote/http2/StreamProcessor.java  |  70 +++++++-
 .../apache/coyote/http2/TestStreamProcessor.java   | 177 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   4 +
 3 files changed, 250 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/coyote/http2/StreamProcessor.java 
b/java/org/apache/coyote/http2/StreamProcessor.java
index bbaf902..c1a943a 100644
--- a/java/org/apache/coyote/http2/StreamProcessor.java
+++ b/java/org/apache/coyote/http2/StreamProcessor.java
@@ -18,9 +18,11 @@ package org.apache.coyote.http2;
 
 import java.io.File;
 import java.io.IOException;
+import java.util.Enumeration;
 import java.util.Iterator;
 
 import jakarta.servlet.ServletConnection;
+import jakarta.servlet.http.HttpServletResponse;
 
 import org.apache.coyote.AbstractProcessor;
 import org.apache.coyote.ActionCode;
@@ -37,6 +39,7 @@ import org.apache.juli.logging.LogFactory;
 import org.apache.tomcat.util.buf.ByteChunk;
 import org.apache.tomcat.util.http.FastHttpDateFormat;
 import org.apache.tomcat.util.http.MimeHeaders;
+import org.apache.tomcat.util.http.parser.HttpParser;
 import org.apache.tomcat.util.net.AbstractEndpoint.Handler.SocketState;
 import org.apache.tomcat.util.net.DispatchType;
 import org.apache.tomcat.util.net.SendfileState;
@@ -412,7 +415,13 @@ class StreamProcessor extends AbstractProcessor {
     @Override
     public final SocketState service(SocketWrapperBase<?> socket) throws 
IOException {
         try {
-            adapter.service(request, response);
+            if (validateRequest()) {
+                adapter.service(request, response);
+            } else {
+                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+                adapter.log(request, response, 0);
+                setErrorState(ErrorState.CLOSE_CLEAN, null);
+            }
         } catch (Exception e) {
             if (log.isDebugEnabled()) {
                 log.debug(sm.getString("streamProcessor.service.error"), e);
@@ -437,6 +446,65 @@ class StreamProcessor extends AbstractProcessor {
     }
 
 
+    /*
+     * In HTTP/1.1 some aspects of the request are validated as the request is
+     * parsed and the request rejected immediately with a 400 response. These
+     * checks are performed in Http11InputBuffer. Because, in Tomcat's HTTP/2
+     * implementation, incoming frames are processed on one thread while the
+     * corresponding request/response is processed on a separate thread,
+     * rejecting invalid requests is more involved.
+     *
+     * One approach would be to validate the request during parsing, note any
+     * validation errors and then generate a 400 response once processing moves
+     * to the separate request/response thread. This would require refactoring
+     * to track the validation errors.
+     *
+     * A second approach, and the one currently adopted, is to perform the
+     * validation shortly after processing of the received request passes to 
the
+     * separate thread and to generate a 400 response if validation fails.
+     *
+     * The checks performed below are based on the checks in Http11InputBuffer.
+     */
+    private boolean validateRequest() {
+        // Method name must be a token
+        String method = request.method().toString();
+        if (!HttpParser.isToken(method)) {
+            return false;
+        }
+
+        // Invalid character in request target
+        // (other checks such as valid %nn happen later)
+        ByteChunk bc = request.requestURI().getByteChunk();
+        for (int i = bc.getStart(); i < bc.getEnd(); i++) {
+            if (HttpParser.isNotRequestTarget(bc.getBuffer()[i])) {
+                return false;
+            }
+        }
+
+        // Ensure the query string doesn't contain invalid characters.
+        // (other checks such as valid %nn happen later)
+        String qs = request.queryString().toString();
+        if (qs != null) {
+            for (char c : qs.toCharArray()) {
+                if (!HttpParser.isQuery(c)) {
+                    return false;
+                }
+            }
+        }
+
+        // HTTP header names must be tokens.
+        MimeHeaders headers = request.getMimeHeaders();
+        Enumeration<String> names = headers.names();
+        while (names.hasMoreElements()) {
+            if (!HttpParser.isToken(names.nextElement())) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+
     @Override
     protected final boolean flushBufferedWrite() throws IOException {
         if (log.isDebugEnabled()) {
diff --git a/test/org/apache/coyote/http2/TestStreamProcessor.java 
b/test/org/apache/coyote/http2/TestStreamProcessor.java
index 6628d0f..2fd8c94 100644
--- a/test/org/apache/coyote/http2/TestStreamProcessor.java
+++ b/test/org/apache/coyote/http2/TestStreamProcessor.java
@@ -169,6 +169,183 @@ public class TestStreamProcessor extends Http2TestBase {
     }
 
 
+    @Test
+    public void testValidateRequestMethod() throws Exception {
+        enableHttp2();
+
+        Tomcat tomcat = getTomcatInstance();
+
+        File appDir = new File("test/webapp");
+        Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath());
+
+        Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
+        ctxt.addServletMappingDecoded("/simple", "simple");
+
+        tomcat.start();
+
+        openClientConnection();
+        doHttpUpgrade();
+        sendClientPreface();
+        validateHttp2InitialResponse();
+
+        byte[] frameHeader = new byte[9];
+        ByteBuffer headersPayload = ByteBuffer.allocate(128);
+
+        List<Header> headers = new ArrayList<>(4);
+        headers.add(new Header(":method", "not,token"));
+        headers.add(new Header(":scheme", "http"));
+        headers.add(new Header(":path", "/index.html"));
+        headers.add(new Header(":authority", "localhost:" + getPort()));
+
+        buildGetRequest(frameHeader, headersPayload, null, headers, 3);
+
+        writeFrame(frameHeader, headersPayload);
+
+        parser.readFrame(true);
+
+        StringBuilder expected = new StringBuilder();
+        expected.append("3-HeadersStart\n");
+        expected.append("3-Header-[:status]-[400]\n");
+        expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n");
+        expected.append("3-HeadersEnd\n");
+
+        Assert.assertEquals(expected.toString(), output.getTrace());
+    }
+
+
+    @Test
+    public void testValidateRequestHeaderName() throws Exception {
+        enableHttp2();
+
+        Tomcat tomcat = getTomcatInstance();
+
+        File appDir = new File("test/webapp");
+        Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath());
+
+        Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
+        ctxt.addServletMappingDecoded("/simple", "simple");
+
+        tomcat.start();
+
+        openClientConnection();
+        doHttpUpgrade();
+        sendClientPreface();
+        validateHttp2InitialResponse();
+
+        byte[] frameHeader = new byte[9];
+        ByteBuffer headersPayload = ByteBuffer.allocate(128);
+
+        List<Header> headers = new ArrayList<>(5);
+        headers.add(new Header(":method", "GET"));
+        headers.add(new Header(":scheme", "http"));
+        headers.add(new Header(":path", "/index.html"));
+        headers.add(new Header(":authority", "localhost:" + getPort()));
+        headers.add(new Header("not token", "value"));
+
+        buildGetRequest(frameHeader, headersPayload, null, headers, 3);
+
+        writeFrame(frameHeader, headersPayload);
+
+        parser.readFrame(true);
+
+        StringBuilder expected = new StringBuilder();
+        expected.append("3-HeadersStart\n");
+        expected.append("3-Header-[:status]-[400]\n");
+        expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n");
+        expected.append("3-HeadersEnd\n");
+
+        Assert.assertEquals(expected.toString(), output.getTrace());
+    }
+
+
+    @Test
+    public void testValidateRequestURI() throws Exception {
+        enableHttp2();
+
+        Tomcat tomcat = getTomcatInstance();
+
+        File appDir = new File("test/webapp");
+        Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath());
+
+        Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
+        ctxt.addServletMappingDecoded("/simple", "simple");
+
+        tomcat.start();
+
+        openClientConnection();
+        doHttpUpgrade();
+        sendClientPreface();
+        validateHttp2InitialResponse();
+
+        byte[] frameHeader = new byte[9];
+        ByteBuffer headersPayload = ByteBuffer.allocate(128);
+
+        List<Header> headers = new ArrayList<>(4);
+        headers.add(new Header(":method", "GET"));
+        headers.add(new Header(":scheme", "http"));
+        headers.add(new Header(":path", "/index^html"));
+        headers.add(new Header(":authority", "localhost:" + getPort()));
+
+        buildGetRequest(frameHeader, headersPayload, null, headers, 3);
+
+        writeFrame(frameHeader, headersPayload);
+
+        parser.readFrame(true);
+
+        StringBuilder expected = new StringBuilder();
+        expected.append("3-HeadersStart\n");
+        expected.append("3-Header-[:status]-[400]\n");
+        expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n");
+        expected.append("3-HeadersEnd\n");
+
+        Assert.assertEquals(expected.toString(), output.getTrace());
+    }
+
+
+    @Test
+    public void testValidateRequestQueryString() throws Exception {
+        enableHttp2();
+
+        Tomcat tomcat = getTomcatInstance();
+
+        File appDir = new File("test/webapp");
+        Context ctxt = tomcat.addWebapp(null, "", appDir.getAbsolutePath());
+
+        Tomcat.addServlet(ctxt, "simple", new SimpleServlet());
+        ctxt.addServletMappingDecoded("/simple", "simple");
+
+        tomcat.start();
+
+        openClientConnection();
+        doHttpUpgrade();
+        sendClientPreface();
+        validateHttp2InitialResponse();
+
+        byte[] frameHeader = new byte[9];
+        ByteBuffer headersPayload = ByteBuffer.allocate(128);
+
+        List<Header> headers = new ArrayList<>(4);
+        headers.add(new Header(":method", "GET"));
+        headers.add(new Header(":scheme", "http"));
+        headers.add(new Header(":path", "/index.html?foo=[]"));
+        headers.add(new Header(":authority", "localhost:" + getPort()));
+
+        buildGetRequest(frameHeader, headersPayload, null, headers, 3);
+
+        writeFrame(frameHeader, headersPayload);
+
+        parser.readFrame(true);
+
+        StringBuilder expected = new StringBuilder();
+        expected.append("3-HeadersStart\n");
+        expected.append("3-Header-[:status]-[400]\n");
+        expected.append("3-Header-[date]-[Wed, 11 Nov 2015 19:18:42 GMT]\n");
+        expected.append("3-HeadersEnd\n");
+
+        Assert.assertEquals(expected.toString(), output.getTrace());
+    }
+
+
     private static final class AsyncComplete extends HttpServlet {
 
         private static final long serialVersionUID = 1L;
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 388893c..4d092c7 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -157,6 +157,10 @@
       <scode>
         Refactor testing whether a String is a valid HTTP token. (markt)
       </scode>
+      <fix>
+        <bug>65785</bug>: Perform additional validation of HTTP headers when
+        using HTTP/2. (markt)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Jasper">

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

Reply via email to