This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push: new c652ffc Fix BZ 65785 - additional HTTP/2 header validation c652ffc is described below commit c652ffcb4e6ed965c007f8c24a2a967034e0e850 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 | 71 ++++++++- .../apache/coyote/http2/TestStreamProcessor.java | 177 +++++++++++++++++++++ webapps/docs/changelog.xml | 4 + 3 files changed, 251 insertions(+), 1 deletion(-) diff --git a/java/org/apache/coyote/http2/StreamProcessor.java b/java/org/apache/coyote/http2/StreamProcessor.java index d1ae2f9..df0eb7f 100644 --- a/java/org/apache/coyote/http2/StreamProcessor.java +++ b/java/org/apache/coyote/http2/StreamProcessor.java @@ -18,8 +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.http.HttpServletResponse; + import org.apache.coyote.AbstractProcessor; import org.apache.coyote.ActionCode; import org.apache.coyote.Adapter; @@ -35,6 +38,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; @@ -410,7 +414,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); @@ -435,6 +445,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 d660638..997b99c 100644 --- a/test/org/apache/coyote/http2/TestStreamProcessor.java +++ b/test/org/apache/coyote/http2/TestStreamProcessor.java @@ -170,6 +170,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 31c1cba..3f38547 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -148,6 +148,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