VYPR
High severity7.1NVD Advisory· Published Mar 20, 2017· Updated May 13, 2026

CVE-2016-6816

CVE-2016-6816

Description

The code in Apache Tomcat 9.0.0.M1 to 9.0.0.M11, 8.5.0 to 8.5.6, 8.0.0.RC1 to 8.0.38, 7.0.0 to 7.0.72, and 6.0.0 to 6.0.47 that parsed the HTTP request line permitted invalid characters. This could be exploited, in conjunction with a proxy that also permitted the invalid characters but with a different interpretation, to inject data into the HTTP response. By manipulating the HTTP response the attacker could poison a web-cache, perform an XSS attack and/or obtain sensitive information from requests other then their own.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
org.apache.tomcat:tomcat-coyoteMaven
>= 9.0.0.M1, < 9.0.0.M129.0.0.M12
org.apache.tomcat:tomcat-coyoteMaven
>= 8.5.0, < 8.5.88.5.8
org.apache.tomcat:tomcat-coyoteMaven
>= 8.0.0RC1, < 8.0.398.0.39
org.apache.tomcat:tomcat-coyoteMaven
>= 7.0.0, < 7.0.737.0.73
org.apache.tomcat:tomcat-coyoteMaven
>= 6.0.0, < 6.0.486.0.48

Affected products

179
  • Apache/Tomcat178 versions
    cpe:2.3:a:apache:tomcat:6.0.8:*:*:*:*:*:*:*+ 177 more
    • cpe:2.3:a:apache:tomcat:6.0.8:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.0:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.1:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.2:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.3:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.4:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.5:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.6:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.7:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.40:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.15:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.16:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.17:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.18:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.19:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.20:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.21:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.22:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.23:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.24:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.25:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.26:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.27:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.57:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.58:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.59:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.60:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.61:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.64:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.65:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.66:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.67:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.17:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.18:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.19:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.20:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.21:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.22:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.23:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.24:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.25:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.26:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.27:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.28:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.29:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.30:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.31:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.32:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.33:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.34:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.35:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.36:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.37:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.38:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.5.0:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.9:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.10:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.11:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.12:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.13:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.14:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.15:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.16:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.17:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.18:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.19:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.20:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.21:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.22:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.23:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.24:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.25:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.26:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.27:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.28:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.29:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.30:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.31:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.32:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.33:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.34:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.35:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.36:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.37:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.38:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.39:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.41:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.42:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.43:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.44:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.45:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.46:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:6.0.47:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.0:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.1:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.2:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.3:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.4:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.5:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.6:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.7:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.8:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.9:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.10:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.11:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.12:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.13:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.14:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.28:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.29:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.30:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.31:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.32:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.33:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.34:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.35:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.36:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.37:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.38:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.39:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.40:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.41:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.42:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.43:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.44:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.45:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.46:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.47:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.48:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.49:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.50:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.51:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.52:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.53:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.54:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.55:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.56:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.62:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.63:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.68:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.69:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.70:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.71:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:7.0.72:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.0:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.1:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.2:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.3:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.4:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.5:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.6:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.7:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.8:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.9:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.10:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.11:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.12:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.13:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.14:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.15:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.0.16:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone1:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone10:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone11:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone2:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.5.1:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.5.2:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.5.3:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.5.4:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.5.5:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:8.5.6:*:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone3:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone4:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone5:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone6:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone7:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone8:*:*:*:*:*:*
    • cpe:2.3:a:apache:tomcat:9.0.0:milestone9:*:*:*:*:*:*
  • Apache Software Foundation/Apache Tomcatv5
    Range: 9.0.0.M1 to 9.0.0.M11

Patches

4
cdc0a935c217

Add additional checks for valid characters to the HTTP request line

https://github.com/apache/tomcatMark ThomasNov 2, 2016via ghsa
7 files changed · +141 134
  • java/org/apache/coyote/http11/AbstractInputBuffer.java+1 53 modified
    @@ -28,62 +28,10 @@
     
     public abstract class AbstractInputBuffer<S> implements InputBuffer{
     
    -    protected static final boolean[] HTTP_TOKEN_CHAR = new boolean[128];
    -
         /**
          * The string manager for this package.
          */
    -    protected static final StringManager sm =
    -        StringManager.getManager(Constants.Package);
    -
    -
    -    static {
    -        for (int i = 0; i < 128; i++) {
    -            if (i < 32) {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == 127) {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '(') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ')') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '<') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '>') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '@') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ',') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ';') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ':') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '\\') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '\"') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '/') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '[') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ']') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '?') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '=') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '{') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '}') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ' ') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else {
    -                HTTP_TOKEN_CHAR[i] = true;
    -            }
    -        }
    -    }
    +    protected static final StringManager sm = StringManager.getManager(Constants.Package);
     
     
         /**
    
  • java/org/apache/coyote/http11/InternalAprInputBuffer.java+29 25 modified
    @@ -30,6 +30,7 @@
     import org.apache.tomcat.jni.Status;
     import org.apache.tomcat.util.buf.ByteChunk;
     import org.apache.tomcat.util.buf.MessageBytes;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.AbstractEndpoint;
     import org.apache.tomcat.util.net.SocketWrapper;
     
    @@ -70,7 +71,7 @@ public InternalAprInputBuffer(Request request, int headerBufferSize) {
     
             parsingHeader = true;
             swallowInput = true;
    -        
    +
         }
     
     
    @@ -93,7 +94,7 @@ public InternalAprInputBuffer(Request request, int headerBufferSize) {
         // --------------------------------------------------------- Public Methods
     
         /**
    -     * Recycle the input buffer. This should be called when closing the 
    +     * Recycle the input buffer. This should be called when closing the
          * connection.
          */
         @Override
    @@ -105,14 +106,14 @@ public void recycle() {
     
     
         /**
    -     * Read the request line. This function is meant to be used during the 
    -     * HTTP request header parsing. Do NOT attempt to read the request body 
    +     * Read the request line. This function is meant to be used during the
    +     * HTTP request header parsing. Do NOT attempt to read the request body
          * using it.
          *
          * @throws IOException If an exception occurs during the underlying socket
          * read operations, or if the given buffer is not big enough to accommodate
          * the whole line.
    -     * @return true if data is properly fed; false if no data is available 
    +     * @return true if data is properly fed; false if no data is available
          * immediately and thread should be freed
          */
         @Override
    @@ -177,7 +178,7 @@ public boolean parseRequestLine(boolean useAvailableData)
                 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
                     space = true;
                     request.method().setBytes(buf, start, pos - start);
    -            } else if (!HTTP_TOKEN_CHAR[buf[pos]]) {
    +            } else if (!HttpParser.isToken(buf[pos])) {
                     throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                 }
     
    @@ -222,15 +223,16 @@ public boolean parseRequestLine(boolean useAvailableData)
                 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
                     space = true;
                     end = pos;
    -            } else if ((buf[pos] == Constants.CR) 
    +            } else if ((buf[pos] == Constants.CR)
                            || (buf[pos] == Constants.LF)) {
                     // HTTP/0.9 style request
                     eol = true;
                     space = true;
                     end = pos;
    -            } else if ((buf[pos] == Constants.QUESTION) 
    -                       && (questionPos == -1)) {
    +            } else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) {
                     questionPos = pos;
    +            } else if (HttpParser.isNotRequestTarget(buf[pos])) {
    +                throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
                 }
     
                 pos++;
    @@ -239,7 +241,7 @@ public boolean parseRequestLine(boolean useAvailableData)
     
             request.unparsedURI().setBytes(buf, start, end - start);
             if (questionPos >= 0) {
    -            request.queryString().setBytes(buf, questionPos + 1, 
    +            request.queryString().setBytes(buf, questionPos + 1,
                                                end - questionPos - 1);
                 request.requestURI().setBytes(buf, start, questionPos - start);
             } else {
    @@ -267,7 +269,7 @@ public boolean parseRequestLine(boolean useAvailableData)
     
             //
             // Reading the protocol
    -        // Protocol is always US-ASCII
    +        // Protocol is always "HTTP/" DIGIT "." DIGIT
             //
     
             while (!eol) {
    @@ -284,6 +286,8 @@ public boolean parseRequestLine(boolean useAvailableData)
                     if (end == 0)
                         end = pos;
                     eol = true;
    +            } else if (!HttpParser.isHttpProtocol(buf[pos])) {
    +                throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
                 }
     
                 pos++;
    @@ -295,7 +299,7 @@ public boolean parseRequestLine(boolean useAvailableData)
             } else {
                 request.protocol().setString("");
             }
    -        
    +
             return true;
     
         }
    @@ -324,7 +328,7 @@ public boolean parseHeaders()
     
         /**
          * Parse an HTTP header.
    -     * 
    +     *
          * @return false after reading a blank line (which indicates that the
          * HTTP header parsing is done
          */
    @@ -382,7 +386,7 @@ private boolean parseHeader()
                 if (buf[pos] == Constants.COLON) {
                     colon = true;
                     headerValue = headers.addValue(buf, start, pos - start);
    -            } else if (!HTTP_TOKEN_CHAR[buf[pos]]) {
    +            } else if (!HttpParser.isToken(buf[pos])) {
                     // If a non-token header is detected, skip the line and
                     // ignore the header
                     skipLine(start);
    @@ -488,14 +492,14 @@ private boolean parseHeader()
     
         }
     
    -    
    +
         private void skipLine(int start) throws IOException {
             boolean eol = false;
             int lastRealByte = start;
             if (pos - 1 > start) {
                 lastRealByte = pos - 1;
             }
    -        
    +
             while (!eol) {
     
                 // Read new bytes if needed
    @@ -519,16 +523,16 @@ private void skipLine(int start) throws IOException {
                         lastRealByte - start + 1, Charset.forName("ISO-8859-1"))));
             }
         }
    -    
    -    
    +
    +
         // ---------------------------------------------------- InputBuffer Methods
     
     
         /**
          * Read some bytes.
          */
         @Override
    -    public int doRead(ByteChunk chunk, Request req) 
    +    public int doRead(ByteChunk chunk, Request req)
             throws IOException {
     
             if (lastActiveFilter == -1)
    @@ -556,11 +560,11 @@ protected boolean fill(boolean block) throws IOException {
             // Ignore the block parameter and just call fill
             return fill();
         }
    -    
    -    
    +
    +
         /**
          * Fill the internal buffer using data from the underlying input stream.
    -     * 
    +     *
          * @return false if at end of stream
          */
         protected boolean fill()
    @@ -592,7 +596,7 @@ protected boolean fill()
             } else {
     
                 if (buf.length - end < 4500) {
    -                // In this case, the request header was really large, so we allocate a 
    +                // In this case, the request header was really large, so we allocate a
                     // brand new one; the old one will get GCed when subsequent requests
                     // clear all references
                     buf = new byte[buf.length];
    @@ -638,15 +642,15 @@ protected boolean fill()
          * This class is an input buffer which will read its data from an input
          * stream.
          */
    -    protected class SocketInputBuffer 
    +    protected class SocketInputBuffer
             implements InputBuffer {
     
     
             /**
              * Read bytes into the specified chunk.
              */
             @Override
    -        public int doRead(ByteChunk chunk, Request req ) 
    +        public int doRead(ByteChunk chunk, Request req )
                 throws IOException {
     
                 if (pos >= lastValid) {
    
  • java/org/apache/coyote/http11/InternalInputBuffer.java+22 19 modified
    @@ -28,6 +28,7 @@
     import org.apache.juli.logging.LogFactory;
     import org.apache.tomcat.util.buf.ByteChunk;
     import org.apache.tomcat.util.buf.MessageBytes;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.AbstractEndpoint;
     import org.apache.tomcat.util.net.SocketWrapper;
     
    @@ -69,10 +70,10 @@ public InternalInputBuffer(Request request, int headerBufferSize) {
     
         }
     
    -    
    +
         /**
    -     * Read the request line. This function is meant to be used during the 
    -     * HTTP request header parsing. Do NOT attempt to read the request body 
    +     * Read the request line. This function is meant to be used during the
    +     * HTTP request header parsing. Do NOT attempt to read the request body
          * using it.
          *
          * @throws IOException If an exception occurs during the underlying socket
    @@ -81,7 +82,7 @@ public InternalInputBuffer(Request request, int headerBufferSize) {
          */
         @Override
         public boolean parseRequestLine(boolean useAvailableDataOnly)
    -    
    +
             throws IOException {
     
             int start = 0;
    @@ -131,7 +132,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
                     space = true;
                     request.method().setBytes(buf, start, pos - start);
    -            } else if (!HTTP_TOKEN_CHAR[buf[pos]]) {
    +            } else if (!HttpParser.isToken(buf[pos])) {
                     throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                 }
     
    @@ -176,15 +177,16 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
                     space = true;
                     end = pos;
    -            } else if ((buf[pos] == Constants.CR) 
    +            } else if ((buf[pos] == Constants.CR)
                            || (buf[pos] == Constants.LF)) {
                     // HTTP/0.9 style request
                     eol = true;
                     space = true;
                     end = pos;
    -            } else if ((buf[pos] == Constants.QUESTION) 
    -                       && (questionPos == -1)) {
    +            } else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) {
                     questionPos = pos;
    +            } else if (HttpParser.isNotRequestTarget(buf[pos])) {
    +                throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
                 }
     
                 pos++;
    @@ -193,7 +195,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
     
             request.unparsedURI().setBytes(buf, start, end - start);
             if (questionPos >= 0) {
    -            request.queryString().setBytes(buf, questionPos + 1, 
    +            request.queryString().setBytes(buf, questionPos + 1,
                                                end - questionPos - 1);
                 request.requestURI().setBytes(buf, start, questionPos - start);
             } else {
    @@ -220,9 +222,8 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
     
             //
             // Reading the protocol
    -        // Protocol is always US-ASCII
    +        // Protocol is always "HTTP/" DIGIT "." DIGIT
             //
    -
             while (!eol) {
     
                 // Read new bytes if needed
    @@ -237,6 +238,8 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                     if (end == 0)
                         end = pos;
                     eol = true;
    +            } else if (!HttpParser.isHttpProtocol(buf[pos])) {
    +                throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
                 }
     
                 pos++;
    @@ -248,7 +251,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
             } else {
                 request.protocol().setString("");
             }
    -        
    +
             return true;
     
         }
    @@ -277,7 +280,7 @@ public boolean parseHeaders()
     
         /**
          * Parse an HTTP header.
    -     * 
    +     *
          * @return false after reading a blank line (which indicates that the
          * HTTP header parsing is done
          */
    @@ -335,7 +338,7 @@ private boolean parseHeader()
                 if (buf[pos] == Constants.COLON) {
                     colon = true;
                     headerValue = headers.addValue(buf, start, pos - start);
    -            } else if (!HTTP_TOKEN_CHAR[buf[pos]]) {
    +            } else if (!HttpParser.isToken(buf[pos])) {
                     // If a non-token header is detected, skip the line and
                     // ignore the header
                     skipLine(start);
    @@ -467,7 +470,7 @@ private void skipLine(int start) throws IOException {
             if (pos - 1 > start) {
                 lastRealByte = pos - 1;
             }
    -        
    +
             while (!eol) {
     
                 // Read new bytes if needed
    @@ -494,7 +497,7 @@ private void skipLine(int start) throws IOException {
     
         /**
          * Fill the internal buffer using data from the underlying input stream.
    -     * 
    +     *
          * @return false if at end of stream
          */
         protected boolean fill() throws IOException {
    @@ -521,7 +524,7 @@ protected boolean fill(boolean block) throws IOException {
             } else {
     
                 if (buf.length - end < 4500) {
    -                // In this case, the request header was really large, so we allocate a 
    +                // In this case, the request header was really large, so we allocate a
                     // brand new one; the old one will get GCed when subsequent requests
                     // clear all references
                     buf = new byte[buf.length];
    @@ -548,15 +551,15 @@ protected boolean fill(boolean block) throws IOException {
          * This class is an input buffer which will read its data from an input
          * stream.
          */
    -    protected class InputStreamInputBuffer 
    +    protected class InputStreamInputBuffer
             implements InputBuffer {
     
     
             /**
              * Read bytes into the specified chunk.
              */
             @Override
    -        public int doRead(ByteChunk chunk, Request req ) 
    +        public int doRead(ByteChunk chunk, Request req )
                 throws IOException {
     
                 if (pos >= lastValid) {
    
  • java/org/apache/coyote/http11/InternalNioInputBuffer.java+39 35 modified
    @@ -25,6 +25,7 @@
     import org.apache.coyote.Request;
     import org.apache.tomcat.util.buf.ByteChunk;
     import org.apache.tomcat.util.buf.MessageBytes;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.AbstractEndpoint;
     import org.apache.tomcat.util.net.NioChannel;
     import org.apache.tomcat.util.net.NioEndpoint;
    @@ -92,7 +93,7 @@ enum HeaderParsePosition {
         }
     
         // ----------------------------------------------------------- Constructors
    -    
    +
     
         /**
          * Alternate constructor.
    @@ -137,7 +138,7 @@ public InternalNioInputBuffer(Request request, int headerBufferSize) {
          * Underlying socket.
          */
         private NioChannel socket;
    -    
    +
         /**
          * Selector pool, for blocking reads and blocking writes
          */
    @@ -159,7 +160,7 @@ public InternalNioInputBuffer(Request request, int headerBufferSize) {
         // --------------------------------------------------------- Public Methods
     
         /**
    -     * Recycle the input buffer. This should be called when closing the 
    +     * Recycle the input buffer. This should be called when closing the
          * connection.
          */
         @Override
    @@ -178,7 +179,7 @@ public void recycle() {
     
         /**
          * End processing of current HTTP request.
    -     * Note: All bytes of the current request should have been already 
    +     * Note: All bytes of the current request should have been already
          * consumed. This method only resets all the pointers so that we are ready
          * to parse the next HTTP request.
          */
    @@ -195,14 +196,14 @@ public void nextRequest() {
         }
     
         /**
    -     * Read the request line. This function is meant to be used during the 
    -     * HTTP request header parsing. Do NOT attempt to read the request body 
    +     * Read the request line. This function is meant to be used during the
    +     * HTTP request header parsing. Do NOT attempt to read the request body
          * using it.
          *
          * @throws IOException If an exception occurs during the underlying socket
          * read operations, or if the given buffer is not big enough to accommodate
          * the whole line.
    -     * @return true if data is properly fed; false if no data is available 
    +     * @return true if data is properly fed; false if no data is available
          * immediately and thread should be freed
          */
         @Override
    @@ -217,7 +218,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
             if ( parsingRequestLinePhase == 0 ) {
                 byte chr = 0;
                 do {
    -                
    +
                     // Read new bytes if needed
                     if (pos >= lastValid) {
                         if (useAvailableDataOnly) {
    @@ -262,7 +263,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                     if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
                         space = true;
                         request.method().setBytes(buf, parsingRequestLineStart, pos - parsingRequestLineStart);
    -                } else if (!HTTP_TOKEN_CHAR[buf[pos]]) {
    +                } else if (!HttpParser.isToken(buf[pos])) {
                         throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                     }
                     pos++;
    @@ -289,7 +290,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
             }
             if (parsingRequestLinePhase == 4) {
                 // Mark the current buffer position
    -            
    +
                 int end = 0;
                 //
                 // Reading the URI
    @@ -304,21 +305,22 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                     if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
                         space = true;
                         end = pos;
    -                } else if ((buf[pos] == Constants.CR) 
    +                } else if ((buf[pos] == Constants.CR)
                                || (buf[pos] == Constants.LF)) {
                         // HTTP/0.9 style request
                         parsingRequestLineEol = true;
                         space = true;
                         end = pos;
    -                } else if ((buf[pos] == Constants.QUESTION) 
    -                           && (parsingRequestLineQPos == -1)) {
    +                } else if ((buf[pos] == Constants.QUESTION) && (parsingRequestLineQPos == -1)) {
                         parsingRequestLineQPos = pos;
    +                } else if (HttpParser.isNotRequestTarget(buf[pos])) {
    +                    throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
                     }
                     pos++;
                 }
                 request.unparsedURI().setBytes(buf, parsingRequestLineStart, end - parsingRequestLineStart);
                 if (parsingRequestLineQPos >= 0) {
    -                request.queryString().setBytes(buf, parsingRequestLineQPos + 1, 
    +                request.queryString().setBytes(buf, parsingRequestLineQPos + 1,
                                                    end - parsingRequestLineQPos - 1);
                     request.requestURI().setBytes(buf, parsingRequestLineStart, parsingRequestLineQPos - parsingRequestLineStart);
                 } else {
    @@ -347,28 +349,30 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                 // Mark the current buffer position
                 end = 0;
             }
    -        if (parsingRequestLinePhase == 6) { 
    +        if (parsingRequestLinePhase == 6) {
                 //
                 // Reading the protocol
    -            // Protocol is always US-ASCII
    +            // Protocol is always "HTTP/" DIGIT "." DIGIT
                 //
                 while (!parsingRequestLineEol) {
                     // Read new bytes if needed
                     if (pos >= lastValid) {
                         if (!fill(true, false)) //request line parsing
                             return false;
                     }
    -        
    +
                     if (buf[pos] == Constants.CR) {
                         end = pos;
                     } else if (buf[pos] == Constants.LF) {
                         if (end == 0)
                             end = pos;
                         parsingRequestLineEol = true;
    +                } else if (!HttpParser.isHttpProtocol(buf[pos])) {
    +                    throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
                     }
                     pos++;
                 }
    -        
    +
                 if ( (end - parsingRequestLineStart) > 0) {
                     request.protocol().setBytes(buf, parsingRequestLineStart, end - parsingRequestLineStart);
                 } else {
    @@ -382,7 +386,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
             }
             throw new IllegalStateException("Invalid request line parse phase:"+parsingRequestLinePhase);
         }
    -    
    +
         private void expand(int newsize) {
             if ( newsize > buf.length ) {
                 if (parsingHeader) {
    @@ -397,7 +401,7 @@ private void expand(int newsize) {
                 buf = tmp;
             }
         }
    -    
    +
         /**
          * Perform blocking read with a timeout if desired
          * @param timeout boolean - if we want to use the timeout data
    @@ -406,7 +410,7 @@ private void expand(int newsize) {
          * @throws IOException if a socket exception occurs
          * @throws EOFException if end of stream is reached
          */
    -    
    +
         private int readSocket(boolean timeout, boolean block) throws IOException {
             int nRead = 0;
             socket.getBufHandler().getReadBuffer().clear();
    @@ -428,7 +432,7 @@ private int readSocket(boolean timeout, boolean block) throws IOException {
                             socket.getIOChannel().socket().getSoTimeout());
                 } catch ( EOFException eof ) {
                     nRead = -1;
    -            } finally { 
    +            } finally {
                     if ( selector != null ) pool.put(selector);
                 }
             } else {
    @@ -461,7 +465,7 @@ public boolean parseHeaders()
             }
     
             HeaderParseStatus status = HeaderParseStatus.HAVE_MORE_HEADERS;
    -        
    +
             do {
                 status = parseHeader();
                 // Checking that
    @@ -490,7 +494,7 @@ public boolean parseHeaders()
     
         /**
          * Parse an HTTP header.
    -     * 
    +     *
          * @return false after reading a blank line (which indicates that the
          * HTTP header parsing is done
          */
    @@ -506,7 +510,7 @@ private HeaderParseStatus parseHeader()
     
                 // Read new bytes if needed
                 if (pos >= lastValid) {
    -                if (!fill(true,false)) {//parse header 
    +                if (!fill(true,false)) {//parse header
                         headerParsePos = HeaderParsePosition.HEADER_START;
                         return HeaderParseStatus.NEED_MORE_DATA;
                     }
    @@ -542,7 +546,7 @@ private HeaderParseStatus parseHeader()
     
                 // Read new bytes if needed
                 if (pos >= lastValid) {
    -                if (!fill(true,false)) { //parse header 
    +                if (!fill(true,false)) { //parse header
                         return HeaderParseStatus.NEED_MORE_DATA;
                     }
                 }
    @@ -557,7 +561,7 @@ private HeaderParseStatus parseHeader()
                     headerData.realPos = pos;
                     headerData.lastSignificantChar = pos;
                     break;
    -            } else if (!HTTP_TOKEN_CHAR[chr]) {
    +            } else if (!HttpParser.isToken(chr)) {
                     // If a non-token header is detected, skip the line and
                     // ignore the header
                     headerData.lastSignificantChar = pos;
    @@ -589,7 +593,7 @@ private HeaderParseStatus parseHeader()
                     while (true) {
                         // Read new bytes if needed
                         if (pos >= lastValid) {
    -                        if (!fill(true,false)) {//parse header 
    +                        if (!fill(true,false)) {//parse header
                                 //HEADER_VALUE_START
                                 return HeaderParseStatus.NEED_MORE_DATA;
                             }
    @@ -612,7 +616,7 @@ private HeaderParseStatus parseHeader()
     
                         // Read new bytes if needed
                         if (pos >= lastValid) {
    -                        if (!fill(true,false)) {//parse header 
    +                        if (!fill(true,false)) {//parse header
                                 //HEADER_VALUE
                                 return HeaderParseStatus.NEED_MORE_DATA;
                             }
    @@ -645,7 +649,7 @@ private HeaderParseStatus parseHeader()
                 // Read new bytes if needed
                 if (pos >= lastValid) {
                     if (!fill(true,false)) {//parse header
    -                    
    +
                         //HEADER_MULTI_LINE
                         return HeaderParseStatus.NEED_MORE_DATA;
                     }
    @@ -671,7 +675,7 @@ private HeaderParseStatus parseHeader()
             headerData.recycle();
             return HeaderParseStatus.HAVE_MORE_HEADERS;
         }
    -    
    +
         public int getParsingRequestLinePhase() {
             return parsingRequestLinePhase;
         }
    @@ -770,7 +774,7 @@ protected void init(SocketWrapper<NioChannel> socketWrapper,
     
         /**
          * Fill the internal buffer using data from the underlying input stream.
    -     * 
    +     *
          * @return false if at end of stream
          */
         @Override
    @@ -779,7 +783,7 @@ protected boolean fill(boolean block) throws IOException, EOFException {
         }
     
         protected boolean fill(boolean timeout, boolean block) throws IOException, EOFException {
    -        
    +
     
             boolean read = false;
     
    @@ -808,15 +812,15 @@ protected boolean fill(boolean timeout, boolean block) throws IOException, EOFEx
          * This class is an input buffer which will read its data from an input
          * stream.
          */
    -    protected class SocketInputBuffer 
    +    protected class SocketInputBuffer
             implements InputBuffer {
     
     
             /**
              * Read bytes into the specified chunk.
              */
             @Override
    -        public int doRead(ByteChunk chunk, Request req ) 
    +        public int doRead(ByteChunk chunk, Request req )
                 throws IOException {
     
                 if (pos >= lastValid) {
    
  • java/org/apache/coyote/http11/LocalStrings.properties+3 1 modified
    @@ -42,8 +42,10 @@ http11Processor.upgrade=An internal error has occurred as upgraded connections s
     iib.apr.sslGeneralError=An APR general error was returned by the SSL read operation on APR/native socket [{0}] with wrapper [{1}]. It will be treated as EAGAIN and the socket returned to the poller.
     
     iib.eof.error=Unexpected EOF read on the socket
    -iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 2616 and has been ignored.
    +iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 7230 and has been ignored.
     iib.invalidmethod=Invalid character found in method name. HTTP method names must be tokens
    +iib.invalidRequestTarget=Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
    +iib.invalidHttpProtocol=Invalid character found in the HTTP protocol
     iib.parseheaders.ise.error=Unexpected state: headers already parsed. Buffer not recycled?
     iib.requestheadertoolarge.error=Request header is too large
     
    
  • java/org/apache/tomcat/util/http/parser/HttpParser.java+43 1 modified
    @@ -59,6 +59,8 @@ public class HttpParser {
         private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE];
         private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE];
         private static final boolean[] IS_HEX = new boolean[ARRAY_SIZE];
    +    private static final boolean[] IS_NOT_REQUEST_TARGET = new boolean[ARRAY_SIZE];
    +    private static final boolean[] IS_HTTP_PROTOCOL = new boolean[ARRAY_SIZE];
     
         static {
             // Digest field types.
    @@ -103,6 +105,21 @@ public class HttpParser {
                 if ((i >= '0' && i <='9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) {
                     IS_HEX[i] = true;
                 }
    +
    +            // Not valid for request target.
    +            // Combination of multiple rules from RFC7230 and RFC 3986. Must be
    +            // ASCII, no controls plus a few additional characters excluded
    +            if (IS_CONTROL[i] || i > 127 ||
    +                    i == ' ' || i == '\"' || i == '#' || i == '<' || i == '>' || i == '\\' ||
    +                    i == '^' || i == '`'  || i == '{' || i == '|' || i == '}') {
    +                IS_NOT_REQUEST_TARGET[i] = true;
    +            }
    +
    +            // Not valid for HTTP protocol
    +            // "HTTP/" DIGIT "." DIGIT
    +            if (i == 'H' || i == 'T' || i == 'P' || i == '/' || i == '.' || (i >= '0' && i <= '9')) {
    +                IS_HTTP_PROTOCOL[i] = true;
    +            }
             }
         }
     
    @@ -266,6 +283,7 @@ public static String unquote(String input) {
             return result.toString();
         }
     
    +
         public static boolean isToken(int c) {
             // Fast for correct values, slower for incorrect ones
             try {
    @@ -275,15 +293,39 @@ public static boolean isToken(int c) {
             }
         }
     
    +
         public static boolean isHex(int c) {
    -        // Fast for correct values, slower for incorrect ones
    +        // Fast for correct values, slower for some incorrect ones
             try {
                 return IS_HEX[c];
             } catch (ArrayIndexOutOfBoundsException ex) {
                 return false;
             }
         }
     
    +
    +    public static boolean isNotRequestTarget(int c) {
    +        // Fast for valid request target characters, slower for some incorrect
    +        // ones
    +        try {
    +            return IS_NOT_REQUEST_TARGET[c];
    +        } catch (ArrayIndexOutOfBoundsException ex) {
    +            return true;
    +        }
    +    }
    +
    +
    +    public static boolean isHttpProtocol(int c) {
    +        // Fast for valid HTTP protocol characters, slower for some incorrect
    +        // ones
    +        try {
    +            return IS_HTTP_PROTOCOL[c];
    +        } catch (ArrayIndexOutOfBoundsException ex) {
    +            return false;
    +        }
    +    }
    +
    +
         // Skip any LWS and return the next char
         private static int skipLws(StringReader input, boolean withReset)
                 throws IOException {
    
  • webapps/docs/changelog.xml+4 0 modified
    @@ -122,6 +122,10 @@
             Improve detection of I/O errors during async processing on non-container
             threads and trigger async error handling when they are detected. (markt)
           </fix>
    +      <add>
    +        Add additional checks for valid characters to the HTTP request line
    +        parsing so invalid request lines are rejected sooner. (markt)
    +      </add>
         </changelog>
       </subsection>
       <subsection name="Web applications">
    
779d5d34e68e

Add additional checks for valid characters to the HTTP request line

https://github.com/apache/tomcat80Mark ThomasNov 2, 2016via ghsa
8 files changed · +109 99
  • java/org/apache/coyote/http11/AbstractInputBuffer.java+1 53 modified
    @@ -30,62 +30,10 @@
     
     public abstract class AbstractInputBuffer<S> implements InputBuffer{
     
    -    protected static final boolean[] HTTP_TOKEN_CHAR = new boolean[128];
    -
         /**
          * The string manager for this package.
          */
    -    protected static final StringManager sm =
    -        StringManager.getManager(Constants.Package);
    -
    -
    -    static {
    -        for (int i = 0; i < 128; i++) {
    -            if (i < 32) {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == 127) {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '(') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ')') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '<') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '>') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '@') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ',') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ';') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ':') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '\\') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '\"') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '/') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '[') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ']') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '?') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '=') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '{') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '}') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ' ') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else {
    -                HTTP_TOKEN_CHAR[i] = true;
    -            }
    -        }
    -    }
    +    protected static final StringManager sm = StringManager.getManager(Constants.Package);
     
     
         /**
    
  • java/org/apache/coyote/http11/AbstractNioInputBuffer.java+9 5 modified
    @@ -21,6 +21,7 @@
     
     import org.apache.coyote.Request;
     import org.apache.tomcat.util.buf.MessageBytes;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     
     public abstract class AbstractNioInputBuffer<S> extends AbstractInputBuffer<S> {
     
    @@ -228,7 +229,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                     if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
                         space = true;
                         request.method().setBytes(buf, parsingRequestLineStart, pos - parsingRequestLineStart);
    -                } else if (!HTTP_TOKEN_CHAR[buf[pos]]) {
    +                } else if (!HttpParser.isToken(buf[pos])) {
                         throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                     }
                     pos++;
    @@ -276,9 +277,10 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                         parsingRequestLineEol = true;
                         space = true;
                         end = pos;
    -                } else if ((buf[pos] == Constants.QUESTION)
    -                           && (parsingRequestLineQPos == -1)) {
    +                } else if ((buf[pos] == Constants.QUESTION) && (parsingRequestLineQPos == -1)) {
                         parsingRequestLineQPos = pos;
    +                } else if (HttpParser.isNotRequestTarget(buf[pos])) {
    +                    throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
                     }
                     pos++;
                 }
    @@ -315,7 +317,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
             if (parsingRequestLinePhase == 6) {
                 //
                 // Reading the protocol
    -            // Protocol is always US-ASCII
    +            // Protocol is always "HTTP/" DIGIT "." DIGIT
                 //
                 while (!parsingRequestLineEol) {
                     // Read new bytes if needed
    @@ -330,6 +332,8 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                         if (end == 0)
                             end = pos;
                         parsingRequestLineEol = true;
    +                } else if (!HttpParser.isHttpProtocol(buf[pos])) {
    +                    throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
                     }
                     pos++;
                 }
    @@ -470,7 +474,7 @@ private HeaderParseStatus parseHeader()
                     headerData.realPos = pos;
                     headerData.lastSignificantChar = pos;
                     break;
    -            } else if (chr < 0 || !HTTP_TOKEN_CHAR[chr]) {
    +            } else if (!HttpParser.isToken(chr)) {
                     // If a non-token header is detected, skip the line and
                     // ignore the header
                     headerData.lastSignificantChar = pos;
    
  • java/org/apache/coyote/http11/InternalAprInputBuffer.java+9 5 modified
    @@ -32,6 +32,7 @@
     import org.apache.tomcat.jni.Status;
     import org.apache.tomcat.util.buf.ByteChunk;
     import org.apache.tomcat.util.buf.MessageBytes;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.AbstractEndpoint;
     import org.apache.tomcat.util.net.SocketWrapper;
     
    @@ -181,7 +182,7 @@ public boolean parseRequestLine(boolean useAvailableData)
                 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
                     space = true;
                     request.method().setBytes(buf, start, pos - start);
    -            } else if (!HTTP_TOKEN_CHAR[buf[pos]]) {
    +            } else if (!HttpParser.isToken(buf[pos])) {
                     throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                 }
     
    @@ -232,9 +233,10 @@ public boolean parseRequestLine(boolean useAvailableData)
                     eol = true;
                     space = true;
                     end = pos;
    -            } else if ((buf[pos] == Constants.QUESTION)
    -                       && (questionPos == -1)) {
    +            } else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) {
                     questionPos = pos;
    +            } else if (HttpParser.isNotRequestTarget(buf[pos])) {
    +                throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
                 }
     
                 pos++;
    @@ -270,7 +272,7 @@ public boolean parseRequestLine(boolean useAvailableData)
     
             //
             // Reading the protocol
    -        // Protocol is always US-ASCII
    +        // Protocol is always "HTTP/" DIGIT "." DIGIT
             //
     
             while (!eol) {
    @@ -287,6 +289,8 @@ public boolean parseRequestLine(boolean useAvailableData)
                     if (end == 0)
                         end = pos;
                     eol = true;
    +            } else if (!HttpParser.isHttpProtocol(buf[pos])) {
    +                throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
                 }
     
                 pos++;
    @@ -385,7 +389,7 @@ private boolean parseHeader()
                 if (buf[pos] == Constants.COLON) {
                     colon = true;
                     headerValue = headers.addValue(buf, start, pos - start);
    -            } else if (buf[pos] < 0 || !HTTP_TOKEN_CHAR[buf[pos]]) {
    +            } else if (!HttpParser.isToken(buf[pos])) {
                     // If a non-token header is detected, skip the line and
                     // ignore the header
                     skipLine(start);
    
  • java/org/apache/coyote/http11/InternalInputBuffer.java+9 6 modified
    @@ -28,6 +28,7 @@
     import org.apache.juli.logging.LogFactory;
     import org.apache.tomcat.util.buf.ByteChunk;
     import org.apache.tomcat.util.buf.MessageBytes;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.AbstractEndpoint;
     import org.apache.tomcat.util.net.SocketWrapper;
     
    @@ -142,7 +143,7 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                 if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
                     space = true;
                     request.method().setBytes(buf, start, pos - start);
    -            } else if (!HTTP_TOKEN_CHAR[buf[pos]]) {
    +            } else if (!HttpParser.isToken(buf[pos])) {
                     throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                 }
     
    @@ -193,9 +194,10 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                     eol = true;
                     space = true;
                     end = pos;
    -            } else if ((buf[pos] == Constants.QUESTION)
    -                       && (questionPos == -1)) {
    +            } else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) {
                     questionPos = pos;
    +            } else if (HttpParser.isNotRequestTarget(buf[pos])) {
    +                throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
                 }
     
                 pos++;
    @@ -230,9 +232,8 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
     
             //
             // Reading the protocol
    -        // Protocol is always US-ASCII
    +        // Protocol is always "HTTP/" DIGIT "." DIGIT
             //
    -
             while (!eol) {
     
                 // Read new bytes if needed
    @@ -247,6 +248,8 @@ public boolean parseRequestLine(boolean useAvailableDataOnly)
                     if (end == 0)
                         end = pos;
                     eol = true;
    +            } else if (!HttpParser.isHttpProtocol(buf[pos])) {
    +                throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
                 }
     
                 pos++;
    @@ -345,7 +348,7 @@ private boolean parseHeader()
                 if (buf[pos] == Constants.COLON) {
                     colon = true;
                     headerValue = headers.addValue(buf, start, pos - start);
    -            } else if (buf[pos] < 0 || !HTTP_TOKEN_CHAR[buf[pos]]) {
    +            } else if (!HttpParser.isToken(buf[pos])) {
                     // If a non-token header is detected, skip the line and
                     // ignore the header
                     skipLine(start);
    
  • java/org/apache/coyote/http11/LocalStrings.properties+3 1 modified
    @@ -33,8 +33,10 @@ iib.available.readFail=A non-blocking read failed while attempting to determine
     iib.eof.error=Unexpected EOF read on the socket
     iib.failedread.apr=Read failed with APR/native error code [{0}]
     iib.filter.npe=You may not add a null filter
    -iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 2616 and has been ignored.
    +iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 7230 and has been ignored.
     iib.invalidmethod=Invalid character found in method name. HTTP method names must be tokens
    +iib.invalidRequestTarget=Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
    +iib.invalidHttpProtocol=Invalid character found in the HTTP protocol
     iib.parseheaders.ise.error=Unexpected state: headers already parsed. Buffer not recycled?
     iib.readtimeout=Timeout attempting to read data from the socket
     iib.requestheadertoolarge.error=Request header is too large
    
  • java/org/apache/tomcat/util/http/parser/HttpParser.java+43 1 modified
    @@ -40,6 +40,8 @@ public class HttpParser {
         private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE];
         private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE];
         private static final boolean[] IS_HEX = new boolean[ARRAY_SIZE];
    +    private static final boolean[] IS_NOT_REQUEST_TARGET = new boolean[ARRAY_SIZE];
    +    private static final boolean[] IS_HTTP_PROTOCOL = new boolean[ARRAY_SIZE];
     
         static {
             for (int i = 0; i < ARRAY_SIZE; i++) {
    @@ -65,6 +67,21 @@ public class HttpParser {
                 if ((i >= '0' && i <='9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) {
                     IS_HEX[i] = true;
                 }
    +
    +            // Not valid for request target.
    +            // Combination of multiple rules from RFC7230 and RFC 3986. Must be
    +            // ASCII, no controls plus a few additional characters excluded
    +            if (IS_CONTROL[i] || i > 127 ||
    +                    i == ' ' || i == '\"' || i == '#' || i == '<' || i == '>' || i == '\\' ||
    +                    i == '^' || i == '`'  || i == '{' || i == '|' || i == '}') {
    +                IS_NOT_REQUEST_TARGET[i] = true;
    +            }
    +
    +            // Not valid for HTTP protocol
    +            // "HTTP/" DIGIT "." DIGIT
    +            if (i == 'H' || i == 'T' || i == 'P' || i == '/' || i == '.' || (i >= '0' && i <= '9')) {
    +                IS_HTTP_PROTOCOL[i] = true;
    +            }
             }
         }
     
    @@ -99,6 +116,7 @@ public static String unquote(String input) {
             return result.toString();
         }
     
    +
         public static boolean isToken(int c) {
             // Fast for correct values, slower for incorrect ones
             try {
    @@ -108,15 +126,39 @@ public static boolean isToken(int c) {
             }
         }
     
    +
         public static boolean isHex(int c) {
    -        // Fast for correct values, slower for incorrect ones
    +        // Fast for correct values, slower for some incorrect ones
             try {
                 return IS_HEX[c];
             } catch (ArrayIndexOutOfBoundsException ex) {
                 return false;
             }
         }
     
    +
    +    public static boolean isNotRequestTarget(int c) {
    +        // Fast for valid request target characters, slower for some incorrect
    +        // ones
    +        try {
    +            return IS_NOT_REQUEST_TARGET[c];
    +        } catch (ArrayIndexOutOfBoundsException ex) {
    +            return true;
    +        }
    +    }
    +
    +
    +    public static boolean isHttpProtocol(int c) {
    +        // Fast for valid HTTP protocol characters, slower for some incorrect
    +        // ones
    +        try {
    +            return IS_HTTP_PROTOCOL[c];
    +        } catch (ArrayIndexOutOfBoundsException ex) {
    +            return false;
    +        }
    +    }
    +
    +
         // Skip any LWS and return the next char
         static int skipLws(StringReader input, boolean withReset) throws IOException {
     
    
  • test/org/apache/catalina/valves/rewrite/TestRewriteValve.java+31 28 modified
    @@ -232,7 +232,7 @@ public void testUtf8WithBothQsFlagsRNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,NE]",
    -                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/\u00C2\u00A1\u00C2\u00A1", "id=\u00C2\u00A1");
    +                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null);
         }
     
     
    @@ -242,7 +242,7 @@ public void testUtf8WithBothQsFlagsRBNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,NE]",
    -                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/\u00C2\u00A1%C2%A1", "id=%C2%A1");
    +                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null);
         }
     
     
    @@ -279,8 +279,7 @@ public void testUtf8WithBothQsFlagsRNEQSA() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,NE,QSA]",
    -                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/\u00C2\u00A1\u00C2\u00A1",
    -                "id=\u00C2\u00A1&di=\u00C2\u00AE");
    +                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null);
         }
     
     
    @@ -290,8 +289,7 @@ public void testUtf8WithBothQsFlagsRBNEQSA() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,NE,QSA]",
    -                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/\u00C2\u00A1%C2%A1",
    -                "id=%C2%A1&di=\u00C2\u00AE");
    +                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null);
         }
     
     
    @@ -333,7 +331,7 @@ public void testUtf8WithOriginalQsFlagsRNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,NE]",
    -                "/b/%C2%A1?id=%C2%A1", "/c/\u00C2\u00A1\u00C2\u00A1", "id=\u00C2\u00A1");
    +                "/b/%C2%A1?id=%C2%A1", null);
         }
     
     
    @@ -343,7 +341,7 @@ public void testUtf8WithOriginalQsFlagsRBNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B,NE]",
    -                "/b/%C2%A1?id=%C2%A1", "/c/\u00C2\u00A1%C2%A1", "id=\u00C2\u00A1");
    +                "/b/%C2%A1?id=%C2%A1", null);
         }
     
     
    @@ -394,7 +392,7 @@ public void testUtf8WithRewriteQsFlagsRNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,NE]",
    -                "/b/%C2%A1/id=%C2%A1", "/c/\u00C2\u00A1\u00C2\u00A1", "id=\u00C2\u00A1");
    +                "/b/%C2%A1/id=%C2%A1", null);
         }
     
     
    @@ -404,7 +402,7 @@ public void testUtf8WithRewriteQsFlagsRBNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,NE]",
    -                "/b/%C2%A1/id=%C2%A1", "/c/\u00C2\u00A1%C2%A1", "id=%C2%A1");
    +                "/b/%C2%A1/id=%C2%A1", null);
         }
     
     
    @@ -450,8 +448,7 @@ public void testUtf8FlagsRNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
    -        doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,NE]",
    -                "/b/%C2%A1", "/c/\u00C2\u00A1\u00C2\u00A1");
    +        doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,NE]", "/b/%C2%A1", null);
         }
     
     
    @@ -460,8 +457,7 @@ public void testUtf8FlagsRBNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
    -        doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B,NE]",
    -                "/b/%C2%A1", "/c/\u00C2\u00A1%C2%A1");
    +        doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B,NE]", "/b/%C2%A1", null);
         }
     
     
    @@ -515,22 +511,29 @@ private void doTestRewrite(String config, String request, String expectedURI,
     
             tomcat.start();
     
    -        ByteChunk res = getUrl("http://localhost:" + getPort() + request);
    +        ByteChunk res = new ByteChunk();
    +        int rc = getUrl("http://localhost:" + getPort() + request, res, null);
             res.setCharset(StandardCharsets.UTF_8);
     
    -        String body = res.toString();
    -        RequestDescriptor requestDesc = SnoopResult.parse(body);
    -        String requestURI = requestDesc.getRequestInfo("REQUEST-URI");
    -        Assert.assertEquals(expectedURI, requestURI);
    -
    -        if (expectedQueryString != null) {
    -            String queryString = requestDesc.getRequestInfo("REQUEST-QUERY-STRING");
    -            Assert.assertEquals(expectedQueryString, queryString);
    -        }
    -
    -        if (expectedAttributeValue != null) {
    -            String attributeValue = requestDesc.getAttribute("X-Test");
    -            Assert.assertEquals(expectedAttributeValue, attributeValue);
    +        if (expectedURI == null) {
    +            // Rewrite is expected to fail. Probably because invalid characters
    +            // were written into the request target
    +            Assert.assertEquals(400, rc);
    +        } else {
    +            String body = res.toString();
    +            RequestDescriptor requestDesc = SnoopResult.parse(body);
    +            String requestURI = requestDesc.getRequestInfo("REQUEST-URI");
    +            Assert.assertEquals(expectedURI, requestURI);
    +
    +            if (expectedQueryString != null) {
    +                String queryString = requestDesc.getRequestInfo("REQUEST-QUERY-STRING");
    +                Assert.assertEquals(expectedQueryString, queryString);
    +            }
    +
    +            if (expectedAttributeValue != null) {
    +                String attributeValue = requestDesc.getAttribute("X-Test");
    +                Assert.assertEquals(expectedAttributeValue, attributeValue);
    +            }
             }
         }
     }
    
  • webapps/docs/changelog.xml+4 0 modified
    @@ -97,6 +97,10 @@
             Improve detection of I/O errors during async processing on non-container
             threads and trigger async error handling when they are detected. (markt)
           </fix>
    +      <add>
    +        Add additional checks for valid characters to the HTTP request line
    +        parsing so invalid request lines are rejected sooner. (markt)
    +      </add>
         </changelog>
       </subsection>
       <subsection name="Web applications">
    
f96f5751d418

Add additional checks for valid characters to the HTTP request line

https://github.com/apache/tomcatMark ThomasNov 2, 2016via ghsa
5 files changed · +89 83
  • java/org/apache/coyote/http11/Http11InputBuffer.java+8 53 modified
    @@ -28,6 +28,7 @@
     import org.apache.tomcat.util.buf.ByteChunk;
     import org.apache.tomcat.util.buf.MessageBytes;
     import org.apache.tomcat.util.http.MimeHeaders;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.ApplicationBufferHandler;
     import org.apache.tomcat.util.net.SocketWrapperBase;
     import org.apache.tomcat.util.res.StringManager;
    @@ -48,56 +49,6 @@ public class Http11InputBuffer implements InputBuffer, ApplicationBufferHandler
         private static final StringManager sm = StringManager.getManager(Http11InputBuffer.class);
     
     
    -    private static final boolean[] HTTP_TOKEN_CHAR = new boolean[128];
    -    static {
    -        for (int i = 0; i < 128; i++) {
    -            if (i < 32) {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == 127) {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '(') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ')') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '<') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '>') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '@') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ',') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ';') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ':') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '\\') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '\"') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '/') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '[') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ']') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '?') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '=') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '{') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '}') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ' ') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else {
    -                HTTP_TOKEN_CHAR[i] = true;
    -            }
    -        }
    -    }
    -
    -
         private static final byte[] CLIENT_PREFACE_START =
                 "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1);
     
    @@ -461,7 +412,7 @@ boolean parseRequestLine(boolean keptAlive) throws IOException {
                         space = true;
                         request.method().setBytes(byteBuffer.array(), parsingRequestLineStart,
                                 pos - parsingRequestLineStart);
    -                } else if (!HTTP_TOKEN_CHAR[chr]) {
    +                } else if (!HttpParser.isToken(chr)) {
                         byteBuffer.position(byteBuffer.position() - 1);
                         throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                     }
    @@ -512,6 +463,8 @@ boolean parseRequestLine(boolean keptAlive) throws IOException {
                         end = pos;
                     } else if (chr == Constants.QUESTION && parsingRequestLineQPos == -1) {
                         parsingRequestLineQPos = pos;
    +                } else if (HttpParser.isNotRequestTarget(chr)) {
    +                    throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
                     }
                 }
                 if (parsingRequestLineQPos >= 0) {
    @@ -549,7 +502,7 @@ boolean parseRequestLine(boolean keptAlive) throws IOException {
             if (parsingRequestLinePhase == 6) {
                 //
                 // Reading the protocol
    -            // Protocol is always US-ASCII
    +            // Protocol is always "HTTP/" DIGIT "." DIGIT
                 //
                 while (!parsingRequestLineEol) {
                     // Read new bytes if needed
    @@ -567,6 +520,8 @@ boolean parseRequestLine(boolean keptAlive) throws IOException {
                             end = pos;
                         }
                         parsingRequestLineEol = true;
    +                } else if (!HttpParser.isHttpProtocol(chr)) {
    +                    throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
                     }
                 }
     
    @@ -831,7 +786,7 @@ private HeaderParseStatus parseHeader() throws IOException {
                     headerData.realPos = pos;
                     headerData.lastSignificantChar = pos;
                     break;
    -            } else if (chr < 0 || !HTTP_TOKEN_CHAR[chr]) {
    +            } else if (!HttpParser.isToken(chr)) {
                     // If a non-token header is detected, skip the line and
                     // ignore the header
                     headerData.lastSignificantChar = pos;
    
  • java/org/apache/coyote/http11/LocalStrings.properties+3 1 modified
    @@ -31,8 +31,10 @@ iib.available.readFail=A non-blocking read failed while attempting to determine
     iib.eof.error=Unexpected EOF read on the socket
     iib.failedread.apr=Read failed with APR/native error code [{0}]
     iib.filter.npe=You may not add a null filter
    -iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 2616 and has been ignored.
    +iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 7230 and has been ignored.
     iib.invalidmethod=Invalid character found in method name. HTTP method names must be tokens
    +iib.invalidRequestTarget=Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
    +iib.invalidHttpProtocol=Invalid character found in the HTTP protocol
     iib.parseheaders.ise.error=Unexpected state: headers already parsed. Buffer not recycled?
     iib.readtimeout=Timeout attempting to read data from the socket
     iib.requestheadertoolarge.error=Request header is too large
    
  • java/org/apache/tomcat/util/http/parser/HttpParser.java+43 1 modified
    @@ -40,6 +40,8 @@ public class HttpParser {
         private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE];
         private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE];
         private static final boolean[] IS_HEX = new boolean[ARRAY_SIZE];
    +    private static final boolean[] IS_NOT_REQUEST_TARGET = new boolean[ARRAY_SIZE];
    +    private static final boolean[] IS_HTTP_PROTOCOL = new boolean[ARRAY_SIZE];
     
         static {
             for (int i = 0; i < ARRAY_SIZE; i++) {
    @@ -65,6 +67,21 @@ public class HttpParser {
                 if ((i >= '0' && i <='9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) {
                     IS_HEX[i] = true;
                 }
    +
    +            // Not valid for request target.
    +            // Combination of multiple rules from RFC7230 and RFC 3986. Must be
    +            // ASCII, no controls plus a few additional characters excluded
    +            if (IS_CONTROL[i] || i > 127 ||
    +                    i == ' ' || i == '\"' || i == '#' || i == '<' || i == '>' || i == '\\' ||
    +                    i == '^' || i == '`'  || i == '{' || i == '|' || i == '}') {
    +                IS_NOT_REQUEST_TARGET[i] = true;
    +            }
    +
    +            // Not valid for HTTP protocol
    +            // "HTTP/" DIGIT "." DIGIT
    +            if (i == 'H' || i == 'T' || i == 'P' || i == '/' || i == '.' || (i >= '0' && i <= '9')) {
    +                IS_HTTP_PROTOCOL[i] = true;
    +            }
             }
         }
     
    @@ -99,6 +116,7 @@ public static String unquote(String input) {
             return result.toString();
         }
     
    +
         public static boolean isToken(int c) {
             // Fast for correct values, slower for incorrect ones
             try {
    @@ -108,15 +126,39 @@ public static boolean isToken(int c) {
             }
         }
     
    +
         public static boolean isHex(int c) {
    -        // Fast for correct values, slower for incorrect ones
    +        // Fast for correct values, slower for some incorrect ones
             try {
                 return IS_HEX[c];
             } catch (ArrayIndexOutOfBoundsException ex) {
                 return false;
             }
         }
     
    +
    +    public static boolean isNotRequestTarget(int c) {
    +        // Fast for valid request target characters, slower for some incorrect
    +        // ones
    +        try {
    +            return IS_NOT_REQUEST_TARGET[c];
    +        } catch (ArrayIndexOutOfBoundsException ex) {
    +            return true;
    +        }
    +    }
    +
    +
    +    public static boolean isHttpProtocol(int c) {
    +        // Fast for valid HTTP protocol characters, slower for some incorrect
    +        // ones
    +        try {
    +            return IS_HTTP_PROTOCOL[c];
    +        } catch (ArrayIndexOutOfBoundsException ex) {
    +            return false;
    +        }
    +    }
    +
    +
         // Skip any LWS and return the next char
         static int skipLws(StringReader input, boolean withReset) throws IOException {
     
    
  • test/org/apache/catalina/valves/rewrite/TestRewriteValve.java+31 28 modified
    @@ -232,7 +232,7 @@ public void testUtf8WithBothQsFlagsRNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,NE]",
    -                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/\u00C2\u00A1\u00C2\u00A1", "id=\u00C2\u00A1");
    +                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null);
         }
     
     
    @@ -242,7 +242,7 @@ public void testUtf8WithBothQsFlagsRBNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,NE]",
    -                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/\u00C2\u00A1%C2%A1", "id=%C2%A1");
    +                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null);
         }
     
     
    @@ -279,8 +279,7 @@ public void testUtf8WithBothQsFlagsRNEQSA() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,NE,QSA]",
    -                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/\u00C2\u00A1\u00C2\u00A1",
    -                "id=\u00C2\u00A1&di=\u00C2\u00AE");
    +                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null);
         }
     
     
    @@ -290,8 +289,7 @@ public void testUtf8WithBothQsFlagsRBNEQSA() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,NE,QSA]",
    -                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", "/c/\u00C2\u00A1%C2%A1",
    -                "id=%C2%A1&di=\u00C2\u00AE");
    +                "/b/%C2%A1/id=%C2%A1?di=%C2%AE", null);
         }
     
     
    @@ -333,7 +331,7 @@ public void testUtf8WithOriginalQsFlagsRNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,NE]",
    -                "/b/%C2%A1?id=%C2%A1", "/c/\u00C2\u00A1\u00C2\u00A1", "id=\u00C2\u00A1");
    +                "/b/%C2%A1?id=%C2%A1", null);
         }
     
     
    @@ -343,7 +341,7 @@ public void testUtf8WithOriginalQsFlagsRBNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B,NE]",
    -                "/b/%C2%A1?id=%C2%A1", "/c/\u00C2\u00A1%C2%A1", "id=\u00C2\u00A1");
    +                "/b/%C2%A1?id=%C2%A1", null);
         }
     
     
    @@ -394,7 +392,7 @@ public void testUtf8WithRewriteQsFlagsRNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,NE]",
    -                "/b/%C2%A1/id=%C2%A1", "/c/\u00C2\u00A1\u00C2\u00A1", "id=\u00C2\u00A1");
    +                "/b/%C2%A1/id=%C2%A1", null);
         }
     
     
    @@ -404,7 +402,7 @@ public void testUtf8WithRewriteQsFlagsRBNE() throws Exception {
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
             doTestRewrite("RewriteRule ^/b/(.*)/(.*) /c/\u00A1$1?$2 [R,B,NE]",
    -                "/b/%C2%A1/id=%C2%A1", "/c/\u00C2\u00A1%C2%A1", "id=%C2%A1");
    +                "/b/%C2%A1/id=%C2%A1", null);
         }
     
     
    @@ -450,8 +448,7 @@ public void testUtf8FlagsRNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
    -        doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,NE]",
    -                "/b/%C2%A1", "/c/\u00C2\u00A1\u00C2\u00A1");
    +        doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,NE]", "/b/%C2%A1", null);
         }
     
     
    @@ -460,8 +457,7 @@ public void testUtf8FlagsRBNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
             // header which will be treated as if they are ISO-8859-1
    -        doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B,NE]",
    -                "/b/%C2%A1", "/c/\u00C2\u00A1%C2%A1");
    +        doTestRewrite("RewriteRule ^/b/(.*) /c/\u00A1$1 [R,B,NE]", "/b/%C2%A1", null);
         }
     
     
    @@ -515,22 +511,29 @@ private void doTestRewrite(String config, String request, String expectedURI,
     
             tomcat.start();
     
    -        ByteChunk res = getUrl("http://localhost:" + getPort() + request);
    +        ByteChunk res = new ByteChunk();
    +        int rc = getUrl("http://localhost:" + getPort() + request, res, null);
             res.setCharset(StandardCharsets.UTF_8);
     
    -        String body = res.toString();
    -        RequestDescriptor requestDesc = SnoopResult.parse(body);
    -        String requestURI = requestDesc.getRequestInfo("REQUEST-URI");
    -        Assert.assertEquals(expectedURI, requestURI);
    -
    -        if (expectedQueryString != null) {
    -            String queryString = requestDesc.getRequestInfo("REQUEST-QUERY-STRING");
    -            Assert.assertEquals(expectedQueryString, queryString);
    -        }
    -
    -        if (expectedAttributeValue != null) {
    -            String attributeValue = requestDesc.getAttribute("X-Test");
    -            Assert.assertEquals(expectedAttributeValue, attributeValue);
    +        if (expectedURI == null) {
    +            // Rewrite is expected to fail. Probably because invalid characters
    +            // were written into the request target
    +            Assert.assertEquals(400, rc);
    +        } else {
    +            String body = res.toString();
    +            RequestDescriptor requestDesc = SnoopResult.parse(body);
    +            String requestURI = requestDesc.getRequestInfo("REQUEST-URI");
    +            Assert.assertEquals(expectedURI, requestURI);
    +
    +            if (expectedQueryString != null) {
    +                String queryString = requestDesc.getRequestInfo("REQUEST-QUERY-STRING");
    +                Assert.assertEquals(expectedQueryString, queryString);
    +            }
    +
    +            if (expectedAttributeValue != null) {
    +                String attributeValue = requestDesc.getAttribute("X-Test");
    +                Assert.assertEquals(expectedAttributeValue, attributeValue);
    +            }
             }
         }
     }
    
  • webapps/docs/changelog.xml+4 0 modified
    @@ -151,6 +151,10 @@
             Improve detection of I/O errors during async processing on non-container
             threads and trigger async error handling when they are detected. (markt)
           </fix>
    +      <add>
    +        Add additional checks for valid characters to the HTTP request line
    +        parsing so invalid request lines are rejected sooner. (markt)
    +      </add>
         </changelog>
       </subsection>
       <subsection name="Jasper">
    
516bda676ac8

Add additional checks for valid characters to the HTTP request line

https://github.com/apache/tomcatMark ThomasNov 2, 2016via ghsa
5 files changed · +69 55
  • java/org/apache/coyote/http11/Http11InputBuffer.java+8 53 modified
    @@ -27,6 +27,7 @@
     import org.apache.juli.logging.LogFactory;
     import org.apache.tomcat.util.buf.MessageBytes;
     import org.apache.tomcat.util.http.MimeHeaders;
    +import org.apache.tomcat.util.http.parser.HttpParser;
     import org.apache.tomcat.util.net.ApplicationBufferHandler;
     import org.apache.tomcat.util.net.SocketWrapperBase;
     import org.apache.tomcat.util.res.StringManager;
    @@ -47,56 +48,6 @@ public class Http11InputBuffer implements InputBuffer, ApplicationBufferHandler
         private static final StringManager sm = StringManager.getManager(Http11InputBuffer.class);
     
     
    -    private static final boolean[] HTTP_TOKEN_CHAR = new boolean[128];
    -    static {
    -        for (int i = 0; i < 128; i++) {
    -            if (i < 32) {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == 127) {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '(') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ')') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '<') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '>') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '@') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ',') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ';') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ':') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '\\') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '\"') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '/') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '[') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ']') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '?') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '=') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '{') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == '}') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else if (i == ' ') {
    -                HTTP_TOKEN_CHAR[i] = false;
    -            } else {
    -                HTTP_TOKEN_CHAR[i] = true;
    -            }
    -        }
    -    }
    -
    -
         private static final byte[] CLIENT_PREFACE_START =
                 "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.ISO_8859_1);
     
    @@ -446,7 +397,7 @@ boolean parseRequestLine(boolean keptAlive) throws IOException {
                         space = true;
                         request.method().setBytes(byteBuffer.array(), parsingRequestLineStart,
                                 pos - parsingRequestLineStart);
    -                } else if (!HTTP_TOKEN_CHAR[chr]) {
    +                } else if (!HttpParser.isToken(chr)) {
                         byteBuffer.position(byteBuffer.position() - 1);
                         throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
                     }
    @@ -497,6 +448,8 @@ boolean parseRequestLine(boolean keptAlive) throws IOException {
                         end = pos;
                     } else if (chr == Constants.QUESTION && parsingRequestLineQPos == -1) {
                         parsingRequestLineQPos = pos;
    +                } else if (HttpParser.isNotRequestTarget(chr)) {
    +                    throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
                     }
                 }
                 if (parsingRequestLineQPos >= 0) {
    @@ -534,7 +487,7 @@ boolean parseRequestLine(boolean keptAlive) throws IOException {
             if (parsingRequestLinePhase == 6) {
                 //
                 // Reading the protocol
    -            // Protocol is always US-ASCII
    +            // Protocol is always "HTTP/" DIGIT "." DIGIT
                 //
                 while (!parsingRequestLineEol) {
                     // Read new bytes if needed
    @@ -552,6 +505,8 @@ boolean parseRequestLine(boolean keptAlive) throws IOException {
                             end = pos;
                         }
                         parsingRequestLineEol = true;
    +                } else if (!HttpParser.isHttpProtocol(chr)) {
    +                    throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
                     }
                 }
     
    @@ -816,7 +771,7 @@ private HeaderParseStatus parseHeader() throws IOException {
                     headerData.realPos = pos;
                     headerData.lastSignificantChar = pos;
                     break;
    -            } else if (chr < 0 || !HTTP_TOKEN_CHAR[chr]) {
    +            } else if (chr < 0 || !HttpParser.isToken(chr)) {
                     // If a non-token header is detected, skip the line and
                     // ignore the header
                     headerData.lastSignificantChar = pos;
    
  • java/org/apache/coyote/http11/LocalStrings.properties+3 1 modified
    @@ -31,8 +31,10 @@ iib.available.readFail=A non-blocking read failed while attempting to determine
     iib.eof.error=Unexpected EOF read on the socket
     iib.failedread.apr=Read failed with APR/native error code [{0}]
     iib.filter.npe=You may not add a null filter
    -iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 2616 and has been ignored.
    +iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 7230 and has been ignored.
     iib.invalidmethod=Invalid character found in method name. HTTP method names must be tokens
    +iib.invalidRequestTarget=Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
    +iib.invalidHttpProtocol=Invalid character found in the HTTP protocol
     iib.parseheaders.ise.error=Unexpected state: headers already parsed. Buffer not recycled?
     iib.readtimeout=Timeout attempting to read data from the socket
     iib.requestheadertoolarge.error=Request header is too large
    
  • java/org/apache/tomcat/util/http/parser/HttpParser.java+43 1 modified
    @@ -40,6 +40,8 @@ public class HttpParser {
         private static final boolean[] IS_SEPARATOR = new boolean[ARRAY_SIZE];
         private static final boolean[] IS_TOKEN = new boolean[ARRAY_SIZE];
         private static final boolean[] IS_HEX = new boolean[ARRAY_SIZE];
    +    private static final boolean[] IS_NOT_REQUEST_TARGET = new boolean[ARRAY_SIZE];
    +    private static final boolean[] IS_HTTP_PROTOCOL = new boolean[ARRAY_SIZE];
     
         static {
             for (int i = 0; i < ARRAY_SIZE; i++) {
    @@ -65,6 +67,21 @@ public class HttpParser {
                 if ((i >= '0' && i <='9') || (i >= 'a' && i <= 'f') || (i >= 'A' && i <= 'F')) {
                     IS_HEX[i] = true;
                 }
    +
    +            // Not valid for request target.
    +            // Combination of multiple rules from RFC7230 and RFC 3986. Must be
    +            // ASCII, no controls plus a few additional characters excluded
    +            if (IS_CONTROL[i] || i > 127 ||
    +                    i == ' ' || i == '\"' || i == '#' || i == '<' || i == '>' || i == '\\' ||
    +                    i == '^' || i == '`'  || i == '{' || i == '|' || i == '}') {
    +                IS_NOT_REQUEST_TARGET[i] = true;
    +            }
    +
    +            // Not valid for HTTP protocol
    +            // "HTTP/" DIGIT "." DIGIT
    +            if (i == 'H' || i == 'T' || i == 'P' || i == '/' || i == '.' || (i >= '0' && i <= '9')) {
    +                IS_HTTP_PROTOCOL[i] = true;
    +            }
             }
         }
     
    @@ -99,6 +116,7 @@ public static String unquote(String input) {
             return result.toString();
         }
     
    +
         public static boolean isToken(int c) {
             // Fast for correct values, slower for incorrect ones
             try {
    @@ -108,15 +126,39 @@ public static boolean isToken(int c) {
             }
         }
     
    +
         public static boolean isHex(int c) {
    -        // Fast for correct values, slower for incorrect ones
    +        // Fast for correct values, slower for some incorrect ones
             try {
                 return IS_HEX[c];
             } catch (ArrayIndexOutOfBoundsException ex) {
                 return false;
             }
         }
     
    +
    +    public static boolean isNotRequestTarget(int c) {
    +        // Fast for valid request target characters, slower for some incorrect
    +        // ones
    +        try {
    +            return IS_NOT_REQUEST_TARGET[c];
    +        } catch (ArrayIndexOutOfBoundsException ex) {
    +            return true;
    +        }
    +    }
    +
    +
    +    public static boolean isHttpProtocol(int c) {
    +        // Fast for valid HTTP protocol characters, slower for some incorrect
    +        // ones
    +        try {
    +            return IS_HTTP_PROTOCOL[c];
    +        } catch (ArrayIndexOutOfBoundsException ex) {
    +            return false;
    +        }
    +    }
    +
    +
         // Skip any LWS and return the next char
         static int skipLws(StringReader input, boolean withReset) throws IOException {
     
    
  • test/org/apache/catalina/valves/rewrite/TestRewriteValve.java+11 0 modified
    @@ -19,6 +19,7 @@
     import java.nio.charset.StandardCharsets;
     
     import org.junit.Assert;
    +import org.junit.Ignore;
     import org.junit.Test;
     
     import org.apache.catalina.Context;
    @@ -227,6 +228,7 @@ public void testUtf8WithBothQsFlagsRB() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8WithBothQsFlagsRNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    @@ -237,6 +239,7 @@ public void testUtf8WithBothQsFlagsRNE() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8WithBothQsFlagsRBNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    @@ -274,6 +277,7 @@ public void testUtf8WithBothQsFlagsRBQSA() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8WithBothQsFlagsRNEQSA() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    @@ -285,6 +289,7 @@ public void testUtf8WithBothQsFlagsRNEQSA() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8WithBothQsFlagsRBNEQSA() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    @@ -328,6 +333,7 @@ public void testUtf8WithOriginalQsFlagsRB() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8WithOriginalQsFlagsRNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    @@ -338,6 +344,7 @@ public void testUtf8WithOriginalQsFlagsRNE() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8WithOriginalQsFlagsRBNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    @@ -389,6 +396,7 @@ public void testUtf8WithRewriteQsFlagsRB() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8WithRewriteQsFlagsRNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    @@ -399,6 +407,7 @@ public void testUtf8WithRewriteQsFlagsRNE() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8WithRewriteQsFlagsRBNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    @@ -446,6 +455,7 @@ public void testUtf8FlagsRB() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8FlagsRNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    @@ -456,6 +466,7 @@ public void testUtf8FlagsRNE() throws Exception {
     
     
         @Test
    +    @Ignore // Use of NE results in invalid characters in request-target
         public void testUtf8FlagsRBNE() throws Exception {
             // Note %C2%A1 == \u00A1
             // Failing to escape the redirect means UTF-8 bytes in the Location
    
  • webapps/docs/changelog.xml+4 0 modified
    @@ -174,6 +174,10 @@
             Improve detection of I/O errors during async processing on non-container
             threads and trigger async error handling when they are detected. (markt)
           </fix>
    +      <add>
    +        Add additional checks for valid characters to the HTTP request line
    +        parsing so invalid request lines are rejected sooner. (markt)
    +      </add>
         </changelog>
       </subsection>
       <subsection name="Jasper">
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

66

News mentions

0

No linked articles in our index yet.