Author: markt Date: Wed Jul 27 09:10:11 2011 New Revision: 1151394 URL: http://svn.apache.org/viewvc?rev=1151394&view=rev Log: Fix https://issues.apache.org/bugzilla/show_bug.cgi?id=51557 Ignore HTTP headers that do not comply with RFC 2616 and use header names that are not tokens.
Modified: tomcat/trunk/java/org/apache/coyote/http11/AbstractInputBuffer.java tomcat/trunk/java/org/apache/coyote/http11/InternalAprInputBuffer.java tomcat/trunk/java/org/apache/coyote/http11/InternalInputBuffer.java tomcat/trunk/java/org/apache/coyote/http11/InternalNioInputBuffer.java tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties tomcat/trunk/test/org/apache/coyote/http11/TestInternalInputBuffer.java tomcat/trunk/webapps/docs/changelog.xml Modified: tomcat/trunk/java/org/apache/coyote/http11/AbstractInputBuffer.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/AbstractInputBuffer.java?rev=1151394&r1=1151393&r2=1151394&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/AbstractInputBuffer.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/AbstractInputBuffer.java Wed Jul 27 09:10:11 2011 @@ -27,20 +27,7 @@ import org.apache.tomcat.util.res.String public abstract class AbstractInputBuffer implements InputBuffer{ - public abstract boolean parseRequestLine(boolean useAvailableDataOnly) throws IOException; - - public abstract boolean parseHeaders() throws IOException; - - protected abstract boolean fill(boolean block) throws IOException; - - // -------------------------------------------------------------- Constants - - - // ----------------------------------------------------------- Constructors - - - // -------------------------------------------------------------- Variables - + protected static final boolean[] HTTP_TOKEN_CHAR = new boolean[128]; /** * The string manager for this package. @@ -49,7 +36,55 @@ public abstract class AbstractInputBuffe StringManager.getManager(Constants.Package); - // ----------------------------------------------------- Instance Variables + 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 if (i == '\t') { + HTTP_TOKEN_CHAR[i] = false; + } else { + HTTP_TOKEN_CHAR[i] = true; + } + } + } /** @@ -217,6 +252,13 @@ public abstract class AbstractInputBuffe } + public abstract boolean parseRequestLine(boolean useAvailableDataOnly) throws IOException; + + public abstract boolean parseHeaders() throws IOException; + + protected abstract boolean fill(boolean block) throws IOException; + + // --------------------------------------------------------- Public Methods @@ -308,6 +350,4 @@ public abstract class AbstractInputBuffe return activeFilters[lastActiveFilter].doRead(chunk,req); } - - } Modified: tomcat/trunk/java/org/apache/coyote/http11/InternalAprInputBuffer.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/InternalAprInputBuffer.java?rev=1151394&r1=1151393&r2=1151394&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/InternalAprInputBuffer.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/InternalAprInputBuffer.java Wed Jul 27 09:10:11 2011 @@ -14,17 +14,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package org.apache.coyote.http11; import java.io.EOFException; import java.io.IOException; import java.net.SocketTimeoutException; import java.nio.ByteBuffer; +import java.nio.charset.Charset; import org.apache.coyote.InputBuffer; import org.apache.coyote.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; import org.apache.tomcat.jni.Socket; import org.apache.tomcat.jni.Status; import org.apache.tomcat.util.buf.ByteChunk; @@ -38,9 +39,8 @@ import org.apache.tomcat.util.buf.Messag */ public class InternalAprInputBuffer extends AbstractInputBuffer { - - // -------------------------------------------------------------- Constants - + private static final Log log = + LogFactory.getLog(InternalAprInputBuffer.class); // ----------------------------------------------------------- Constructors @@ -394,6 +394,11 @@ public class InternalAprInputBuffer exte if (buf[pos] == Constants.COLON) { colon = true; headerValue = headers.addValue(buf, start, pos - start); + } else if (!HTTP_TOKEN_CHAR[buf[pos]]) { + // If a non-token header is detected, skip the line and + // ignore the header + skipLine(start); + return true; } chr = buf[pos]; if ((chr >= Constants.A) && (chr <= Constants.Z)) { @@ -496,6 +501,38 @@ public class InternalAprInputBuffer exte } + 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 + if (pos >= lastValid) { + if (!fill()) + throw new EOFException(sm.getString("iib.eof.error")); + } + + if (buf[pos] == Constants.CR) { + // Skip + } else if (buf[pos] == Constants.LF) { + eol = true; + } else { + lastRealByte = pos; + } + pos++; + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("iib.invalidheader", new String(buf, start, + lastRealByte - start + 1, Charset.forName("ISO-8859-1")))); + } + } + + /** * Available bytes (note that due to encoding, this may not correspond ) */ Modified: tomcat/trunk/java/org/apache/coyote/http11/InternalInputBuffer.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/InternalInputBuffer.java?rev=1151394&r1=1151393&r2=1151394&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/InternalInputBuffer.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/InternalInputBuffer.java Wed Jul 27 09:10:11 2011 @@ -14,15 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package org.apache.coyote.http11; import java.io.EOFException; import java.io.IOException; +import java.nio.charset.Charset; import org.apache.coyote.InputBuffer; import org.apache.coyote.Request; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; import org.apache.tomcat.util.buf.ByteChunk; import org.apache.tomcat.util.buf.MessageBytes; @@ -34,6 +35,9 @@ import org.apache.tomcat.util.buf.Messag */ public class InternalInputBuffer extends AbstractInputBuffer { + private static final Log log = LogFactory.getLog(InternalInputBuffer.class); + + /** * Default constructor. */ @@ -316,7 +320,13 @@ public class InternalInputBuffer extends if (buf[pos] == Constants.COLON) { colon = true; headerValue = headers.addValue(buf, start, pos - start); + } else if (!HTTP_TOKEN_CHAR[buf[pos]]) { + // If a non-token header is detected, skip the line and + // ignore the header + skipLine(start); + return true; } + chr = buf[pos]; if ((chr >= Constants.A) && (chr <= Constants.Z)) { buf[pos] = (byte) (chr - Constants.LC_OFFSET); @@ -421,6 +431,37 @@ public class InternalInputBuffer extends // ------------------------------------------------------ Protected Methods + 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 + if (pos >= lastValid) { + if (!fill()) + throw new EOFException(sm.getString("iib.eof.error")); + } + + if (buf[pos] == Constants.CR) { + // Skip + } else if (buf[pos] == Constants.LF) { + eol = true; + } else { + lastRealByte = pos; + } + pos++; + } + + if (log.isDebugEnabled()) { + log.debug(sm.getString("iib.invalidheader", new String(buf, start, + lastRealByte - start + 1, Charset.forName("ISO-8859-1")))); + } + } + /** * Fill the internal buffer using data from the underlying input stream. * Modified: tomcat/trunk/java/org/apache/coyote/http11/InternalNioInputBuffer.java URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/InternalNioInputBuffer.java?rev=1151394&r1=1151393&r2=1151394&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/InternalNioInputBuffer.java (original) +++ tomcat/trunk/java/org/apache/coyote/http11/InternalNioInputBuffer.java Wed Jul 27 09:10:11 2011 @@ -14,8 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - - package org.apache.coyote.http11; import java.io.EOFException; @@ -40,9 +38,6 @@ import org.apache.tomcat.util.net.NioSel */ public class InternalNioInputBuffer extends AbstractInputBuffer { - /** - * Logger. - */ private static final org.apache.juli.logging.Log log = org.apache.juli.logging.LogFactory.getLog(InternalNioInputBuffer.class); @@ -52,7 +47,8 @@ public class InternalNioInputBuffer exte // -------------------------------------------------------------- Constants enum HeaderParseStatus {DONE, HAVE_MORE_HEADERS, NEED_MORE_DATA} - enum HeaderParsePosition {HEADER_START, HEADER_NAME, HEADER_VALUE, HEADER_MULTI_LINE} + enum HeaderParsePosition {HEADER_START, HEADER_NAME, HEADER_VALUE, + HEADER_MULTI_LINE, HEADER_SKIPLINE} // ----------------------------------------------------------- Constructors @@ -561,6 +557,10 @@ public class InternalNioInputBuffer exte if (buf[pos] == Constants.COLON) { headerParsePos = HeaderParsePosition.HEADER_VALUE; headerData.headerValue = headers.addValue(buf, headerData.start, pos - headerData.start); + } else if (!HTTP_TOKEN_CHAR[buf[pos]]) { + // If a non-token header is detected, skip the line and + // ignore the header + return skipLine(); } chr = buf[pos]; if ((chr >= Constants.A) && (chr <= Constants.Z)) { @@ -576,6 +576,10 @@ public class InternalNioInputBuffer exte } + while (headerParsePos == HeaderParsePosition.HEADER_SKIPLINE) { + return skipLine(); + } + // // Reading the header value (which can be spanned over multiple lines) // @@ -673,6 +677,41 @@ public class InternalNioInputBuffer exte return HeaderParseStatus.HAVE_MORE_HEADERS; } + private HeaderParseStatus skipLine() throws IOException { + headerParsePos = HeaderParsePosition.HEADER_SKIPLINE; + boolean eol = false; + + // Reading bytes until the end of the line + while (!eol) { + + // Read new bytes if needed + if (pos >= lastValid) { + if (!fill(true,false)) { + return HeaderParseStatus.NEED_MORE_DATA; + } + } + + if (buf[pos] == Constants.CR) { + // Skip + } else if (buf[pos] == Constants.LF) { + eol = true; + } else { + headerData.lastSignificantChar = pos; + } + + pos++; + } + if (log.isDebugEnabled()) { + log.debug(sm.getString("iib.invalidheader", new String(buf, + headerData.start, + headerData.lastSignificantChar - headerData.start + 1, + DEFAULT_CHARSET))); + } + + headerParsePos = HeaderParsePosition.HEADER_START; + return HeaderParseStatus.HAVE_MORE_HEADERS; + } + protected HeaderParseData headerData = new HeaderParseData(); public static class HeaderParseData { int start = 0; Modified: tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties URL: http://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties?rev=1151394&r1=1151393&r2=1151394&view=diff ============================================================================== --- tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties (original) +++ tomcat/trunk/java/org/apache/coyote/http11/LocalStrings.properties Wed Jul 27 09:10:11 2011 @@ -38,5 +38,6 @@ http11processor.comet.notsupported=The C http11processor.sendfile.error=Error sending data using sendfile. May be caused by invalid request attributes for start/end points iib.eof.error=Unexpected EOF read on the socket -iib.requestheadertoolarge.error=Request header is too large +iib.invalidheader=The HTTP header line [{0}] does not conform to RFC 2616 and has been ignored. iib.invalidmethod=Invalid character (CR or LF) found in method name +iib.requestheadertoolarge.error=Request header is too large Modified: tomcat/trunk/test/org/apache/coyote/http11/TestInternalInputBuffer.java URL: http://svn.apache.org/viewvc/tomcat/trunk/test/org/apache/coyote/http11/TestInternalInputBuffer.java?rev=1151394&r1=1151393&r2=1151394&view=diff ============================================================================== --- tomcat/trunk/test/org/apache/coyote/http11/TestInternalInputBuffer.java (original) +++ tomcat/trunk/test/org/apache/coyote/http11/TestInternalInputBuffer.java Wed Jul 27 09:10:11 2011 @@ -26,6 +26,7 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import org.junit.Test; @@ -124,4 +125,199 @@ public class TestInternalInputBuffer ext } } } + + + @Test + public void testBug51557NoColon() { + + Bug51557Client client = new Bug51557Client("X-Bug51557NoColon"); + client.setPort(getPort()); + + client.doRequest(); + assertTrue(client.isResponse200()); + assertEquals("abcd", client.getResponseBody()); + assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testBug51557Separators() throws Exception { + char httpSeparators[] = new char[] { + '\t', ' ', '\"', '(', ')', ',', '/', ':', ';', '<', + '=', '>', '?', '@', '[', '\\', ']', '{', '}' }; + + for (char s : httpSeparators) { + doTestBug51557Char(s); + tearDown(); + setUp(); + } + } + + + @Test + public void testBug51557Ctl() throws Exception { + for (int i = 0; i < 31; i++) { + doTestBug51557Char((char) i); + tearDown(); + setUp(); + } + doTestBug51557Char((char) 127); + } + + + @Test + public void testBug51557Continuation() { + + Bug51557Client client = new Bug51557Client("X-Bug=51557NoColon", + "foo" + SimpleHttpClient.CRLF + " bar"); + client.setPort(getPort()); + + client.doRequest(); + assertTrue(client.isResponse200()); + assertEquals("abcd", client.getResponseBody()); + assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testBug51557BoundaryStart() { + + Bug51557Client client = new Bug51557Client("=X-Bug51557", + "invalid"); + client.setPort(getPort()); + + client.doRequest(); + assertTrue(client.isResponse200()); + assertEquals("abcd", client.getResponseBody()); + assertTrue(client.isResponseBodyOK()); + } + + + @Test + public void testBug51557BoundaryEnd() { + + Bug51557Client client = new Bug51557Client("X-Bug51557=", + "invalid"); + client.setPort(getPort()); + + client.doRequest(); + assertTrue(client.isResponse200()); + assertEquals("abcd", client.getResponseBody()); + assertTrue(client.isResponseBodyOK()); + } + + + private void doTestBug51557Char(char s) { + Bug51557Client client = + new Bug51557Client("X-Bug" + s + "51557", "invalid"); + + client.setPort(getPort()); + client.doRequest(); + assertTrue(client.isResponse200()); + assertEquals("abcd", client.getResponseBody()); + assertTrue(client.isResponseBodyOK()); + } + + /** + * Bug 51557 test client. + */ + private class Bug51557Client extends SimpleHttpClient { + + private String headerName; + private String headerLine; + + public Bug51557Client(String headerName) { + this.headerName = headerName; + this.headerLine = headerName; + } + + public Bug51557Client(String headerName, String headerValue) { + this.headerName = headerName; + this.headerLine = headerName + ": " + headerValue; + } + + private Exception doRequest() { + + Tomcat tomcat = getTomcatInstance(); + + Context root = tomcat.addContext("", TEMP_DIR); + Tomcat.addServlet(root, "Bug51557", + new Bug51557Servlet(headerName)); + root.addServletMapping("/test", "Bug51557"); + + try { + tomcat.start(); + + // Open connection + connect(); + + String[] request = new String[1]; + request[0] = + "GET http://localhost:8080/test HTTP/1.1" + CRLF + + headerLine + CRLF + + "X-Bug51557: abcd" + CRLF + + "Connection: close" + CRLF + + CRLF; + + setRequest(request); + processRequest(); // blocks until response has been read + + // Close the connection + disconnect(); + } catch (Exception e) { + return e; + } + return null; + } + + @Override + public boolean isResponseBodyOK() { + if (getResponseBody() == null) { + return false; + } + if (!getResponseBody().contains("abcd")) { + return false; + } + return true; + } + + } + + private static class Bug51557Servlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + + private String invalidHeaderName; + + /** + * @param invalidHeaderName The header name should be invalid and + * therefore ignored by the header parsing code + */ + public Bug51557Servlet(String invalidHeaderName) { + this.invalidHeaderName = invalidHeaderName; + } + + /** + * Only interested in the request headers from a GET request + */ + @Override + protected void doGet(HttpServletRequest req, HttpServletResponse resp) + throws ServletException, IOException { + // Just echo the header value back as plain text + resp.setContentType("text/plain"); + + PrintWriter out = resp.getWriter(); + + processHeaders(invalidHeaderName, req, out); + processHeaders("X-Bug51557", req, out); + } + + private void processHeaders(String header, HttpServletRequest req, + PrintWriter out) { + Enumeration<String> values = req.getHeaders(header); + while (values.hasMoreElements()) { + out.println(values.nextElement()); + } + } + } } Modified: tomcat/trunk/webapps/docs/changelog.xml URL: http://svn.apache.org/viewvc/tomcat/trunk/webapps/docs/changelog.xml?rev=1151394&r1=1151393&r2=1151394&view=diff ============================================================================== --- tomcat/trunk/webapps/docs/changelog.xml (original) +++ tomcat/trunk/webapps/docs/changelog.xml Wed Jul 27 09:10:11 2011 @@ -116,6 +116,10 @@ <bug>51503</bug>: Add additional validation that prevents a connector from starting if it does not have a port > 0. (markt) </fix> + <fix> + <bug>51557</bug>: Ignore HTTP headers that do not comply with RFC 2616 + and use header names that are not tokens. (markt) + </fix> </changelog> </subsection> <subsection name="Jasper"> --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org For additional commands, e-mail: dev-h...@tomcat.apache.org