diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicLineParser.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicLineParser.java index b7b428253..66d4622d3 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicLineParser.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BasicLineParser.java @@ -193,6 +193,7 @@ public Header parseHeader(final CharArrayBuffer buffer) throws ParseException { if (cursor.getPos() == cursor.getLowerBound() || cursor.getPos() == cursor.getUpperBound() || buffer.charAt(cursor.getPos()) != ':' || TextUtils.isEmpty(name) || + !FieldNameSupport.isToken(name) || Tokenizer.isWhitespace(buffer.charAt(cursor.getPos() - 1))) { throw new ParseException("Invalid header", buffer, cursor.getLowerBound(), cursor.getUpperBound(), cursor.getPos()); diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BufferedHeader.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BufferedHeader.java index 8e73ecf9c..a0673ba47 100644 --- a/httpcore5/src/main/java/org/apache/hc/core5/http/message/BufferedHeader.java +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/BufferedHeader.java @@ -98,7 +98,7 @@ public BufferedHeader(final CharArrayBuffer buffer) throws ParseException { throw new ParseException("Invalid header", buffer, 0, buffer.length(), colon - 1); } final String s = buffer.substringTrimmed(0, colon); - if (s.isEmpty()) { + if (s.isEmpty() || !FieldNameSupport.isToken(s)) { throw new ParseException("Invalid header", buffer, 0, buffer.length(), colon); } this.buffer = buffer; diff --git a/httpcore5/src/main/java/org/apache/hc/core5/http/message/FieldNameSupport.java b/httpcore5/src/main/java/org/apache/hc/core5/http/message/FieldNameSupport.java new file mode 100644 index 000000000..9740ace8a --- /dev/null +++ b/httpcore5/src/main/java/org/apache/hc/core5/http/message/FieldNameSupport.java @@ -0,0 +1,66 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.hc.core5.http.message; + +/** + * Validation of header field names. + */ +final class FieldNameSupport { + + private static final boolean[] TCHAR = new boolean[128]; + + static { + for (int ch = '0'; ch <= '9'; ch++) { + TCHAR[ch] = true; + } + for (int ch = 'a'; ch <= 'z'; ch++) { + TCHAR[ch] = true; + TCHAR[ch - 'a' + 'A'] = true; + } + for (final char ch : "!#$%&'*+-.^_`|~".toCharArray()) { + TCHAR[ch] = true; + } + } + + private FieldNameSupport() { + } + + /** + * Tells whether the given non-empty name consists of {@code tchar}s only. + */ + static boolean isToken(final CharSequence name) { + for (int i = 0; i < name.length(); i++) { + final char ch = name.charAt(i); + if (ch >= TCHAR.length || !TCHAR[ch]) { + return false; + } + } + return true; + } + +} diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestRequestParser.java b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestRequestParser.java index bc65c754c..40a516da7 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestRequestParser.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/impl/io/TestRequestParser.java @@ -33,6 +33,7 @@ import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.Method; +import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.RequestHeaderFieldsTooLargeException; import org.apache.hc.core5.http.config.Http1Config; import org.apache.hc.core5.http.io.SessionInputBuffer; @@ -147,5 +148,34 @@ void testMessageParsingTimeout() throws Exception { Assertions.assertEquals(3, headers.length); } -} + @Test + void testObsFoldedHeaderValue() throws Exception { + final String s = + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + "X-Fold: a\r\n" + + " \tb\r\n" + + "\r\n"; + final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.US_ASCII)); + final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16, StandardCharsets.US_ASCII.newDecoder()); + final ClassicHttpRequest httprequest = new DefaultHttpRequestParser().parse(inBuffer, inputStream); + Assertions.assertEquals("a b", httprequest.getFirstHeader("X-Fold").getValue()); + } + + @Test + void testNonTokenHeaderNameRejected() { + for (final String name : new String[] {"Foo Bar", "X\u0001Y", "Foo\u007f", "Foo\tBar"}) { + final String s = + "GET / HTTP/1.1\r\n" + + "Host: localhost\r\n" + + name + ": v\r\n" + + "\r\n"; + final ByteArrayInputStream inputStream = new ByteArrayInputStream(s.getBytes(StandardCharsets.ISO_8859_1)); + final SessionInputBuffer inBuffer = new SessionInputBufferImpl(16, StandardCharsets.US_ASCII.newDecoder()); + final DefaultHttpRequestParser parser = new DefaultHttpRequestParser(); + Assertions.assertThrows(ParseException.class, () -> parser.parse(inBuffer, inputStream)); + } + } + +} diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicLineParser.java b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicLineParser.java index 30f7b39dd..a128ecb30 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicLineParser.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBasicLineParser.java @@ -36,6 +36,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * Tests for {@link BasicLineParser}. @@ -312,4 +314,28 @@ void testInvalidHeaderParsing() { Assertions.assertThrows(ParseException.class, () -> parser.parseHeader(buffer)); } + @ParameterizedTest + @ValueSource(strings = { + "Content-Length", "X_Custom", "X.Custom", "X-1", "a", "ABCxyz09", + "!#$%&'*+-.^_`|~"}) + void testTokenHeaderName(final String name) throws Exception { + final CharArrayBuffer buf = new CharArrayBuffer(32); + buf.append(name); + buf.append(": value"); + Assertions.assertEquals(name, parser.parseHeader(buf).getName()); + } + + @ParameterizedTest + @ValueSource(strings = { + "Foo Bar", "Foo\tBar", "X\u0001Y", "X\u0000Y", "Foo\rBar", "Foo\nBar", "Foo\u007f", + "F\u00e9", "F\u0100", "F\u0080", + "Foo;", "Foo(", "Foo)", "Foo,", "Foo/", "Foo<", "Foo=", "Foo>", "Foo?", "Foo@", + "Foo[", "Foo\\", "Foo]", "Foo{", "Foo}", "Foo\""}) + void testNonTokenHeaderNameRejected(final String name) { + final CharArrayBuffer buf = new CharArrayBuffer(32); + buf.append(name); + buf.append(": value"); + Assertions.assertThrows(ParseException.class, () -> parser.parseHeader(buf)); + } + } diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBufferedHeader.java b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBufferedHeader.java index 99409f4b0..da13604bd 100644 --- a/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBufferedHeader.java +++ b/httpcore5/src/test/java/org/apache/hc/core5/http/message/TestBufferedHeader.java @@ -36,6 +36,8 @@ import org.apache.hc.core5.util.CharArrayBuffer; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * Unit tests for {@link BufferedHeader}. @@ -119,5 +121,43 @@ void testCRLFNullInHeaderValue() throws Exception { Assertions.assertEquals("blah blah", header3.getValue()); } -} + @ParameterizedTest + @ValueSource(strings = { + "Content-Length", "X_Custom", "X.Custom", "X-1", "a", "ABCxyz09", + "!#$%&'*+-.^_`|~"}) + void testTokenName(final String name) throws Exception { + final CharArrayBuffer buf = new CharArrayBuffer(32); + buf.append(name); + buf.append(": value"); + Assertions.assertEquals(name, new BufferedHeader(buf, true).getName()); + Assertions.assertEquals(name, new BufferedHeader(buf, false).getName()); + } + + @ParameterizedTest + @ValueSource(strings = { + "Foo Bar", "Foo\tBar", "X\u0001Y", "X\u0000Y", "Foo\rBar", "Foo\nBar", "Foo\u007f", + "F\u00e9", "F\u0100", "F\u0080", + "Foo;", "Foo(", "Foo)", "Foo,", "Foo/", "Foo<", "Foo=", "Foo>", "Foo?", "Foo@", + "Foo[", "Foo\\", "Foo]", "Foo{", "Foo}", "Foo\""}) + void testNonTokenNameRejected(final String name) { + final CharArrayBuffer buf = new CharArrayBuffer(32); + buf.append(name); + buf.append(": value"); + Assertions.assertThrows(ParseException.class, () -> new BufferedHeader(buf, true)); + Assertions.assertThrows(ParseException.class, () -> new BufferedHeader(buf, false)); + } + @Test + void testWhitespaceAroundNamePreserved() throws Exception { + // Whitespace before the colon is rejected in strict mode and trimmed in lax mode. + final CharArrayBuffer buf = new CharArrayBuffer(32); + buf.append("Foo : v"); + Assertions.assertThrows(ParseException.class, () -> new BufferedHeader(buf, true)); + Assertions.assertEquals("Foo", new BufferedHeader(buf, false).getName()); + // Leading whitespace is trimmed; a folded line never reaches the header parser on its own. + final CharArrayBuffer buf2 = new CharArrayBuffer(32); + buf2.append(" Foo: v"); + Assertions.assertEquals("Foo", new BufferedHeader(buf2, true).getName()); + } + +}