This is an automated email from the ASF dual-hosted git repository.
markt pushed a commit to branch 8.5.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/8.5.x by this push:
new 7212b3b Fix BZ 65785 - additional HTTP/2 header validation
7212b3b is described below
commit 7212b3b10dd48f83d1b4c29265751874e02b7476
Author: Mark Thomas <[email protected]>
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 95af082..4d1e41c 100644
--- a/java/org/apache/coyote/http2/StreamProcessor.java
+++ b/java/org/apache/coyote/http2/StreamProcessor.java
@@ -17,8 +17,11 @@
package org.apache.coyote.http2;
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;
@@ -34,6 +37,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.SocketEvent;
@@ -373,7 +377,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);
@@ -396,6 +406,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 6f199f7..1c77719 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -204,6 +204,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: [email protected]
For additional commands, e-mail: [email protected]