This is an automated email from the ASF dual-hosted git repository.
pjfanning pushed a commit to branch 1.4.x
in repository https://gitbox.apache.org/repos/asf/pekko-http.git
The following commit(s) were added to refs/heads/1.4.x by this push:
new b396b633b http/2: reject a header field carrying CR, LF or NUL (#1297)
(1.4.x) (#1330)
b396b633b is described below
commit b396b633bed9d9e1537cc3e97149092de41ea062
Author: PJ Fanning <[email protected]>
AuthorDate: Wed Oct 7 15:27:26 2026 +0100
http/2: reject a header field carrying CR, LF or NUL (#1297) (1.4.x) (#1330)
Motivation:
A regular HTTP/2 header field whose value contains CR LF was silently
accepted with the value truncated: `RequestParsing.parseHeaderPair` reuses
the HTTP/1.1 line parser by building `name + ": " + value + "\r\nx"`, so the
parser stops at the first CRLF it meets, which is now the peer's. RFC 9113
8.2.1 says a field name or value carrying NUL, CR or LF makes the message
malformed.
Modification:
Adapted backport of 40b07a21d (#1297). Check every field in the HPACK
listener before it is dispatched on its name and fail with an
Http2ProtocolException, which `HeaderDecompression` turns into
GOAWAY(PROTOCOL_ERROR) - the way 1.4.x already treats other malformed
request headers. On main the same check answers the stream with a 400 and
keeps the connection, but that relies on #961 (ParsedHeadersFrame error
info, RequestErrorFlow) and #1251, which are not on 1.4.x.
Result:
A request whose header field name or value contains CR, LF or NUL is no
longer accepted with a truncated value; the connection is closed with
GOAWAY(PROTOCOL_ERROR) and the request never reaches the handler.
---
.../engine/http2/hpack/HeaderDecompression.scala | 11 +++++++++
.../http/impl/engine/http2/Http2ServerSpec.scala | 21 +++++++++++++++++
.../impl/engine/http2/RequestParsingSpec.scala | 27 ++++++++++++++++++++++
3 files changed, 59 insertions(+)
diff --git
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala
index fc7aafb36..ab4d211af 100644
---
a/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala
+++
b/http-core/src/main/scala/org/apache/pekko/http/impl/engine/http2/hpack/HeaderDecompression.scala
@@ -74,6 +74,14 @@ private[http2] final class
HeaderDecompression(masterHeaderParser: HttpHeaderPar
val headers = new VectorBuilder[(String, AnyRef)]
object Receiver extends HeaderListener {
def addHeader(name: String, value: String, parsed: AnyRef,
sensitive: Boolean): AnyRef = {
+ // RFC 9113 8.2.1: a field name or value carrying a NUL, CR or LF
makes the message malformed. Check it
+ // here, before the field is dispatched on its name: a regular
field goes through the HTTP/1.1 line
+ // parser, which reads up to the first CRLF it finds and would
silently accept the value truncated
+ // there. Neither the name nor the value is echoed, since either
may be what is malformed.
+ if (HeaderCompression.hasIllegalChar(name))
+ throw new Http2ProtocolException("Malformed request: header
field name must not contain CR, LF or NUL")
+ if (HeaderCompression.hasIllegalChar(value))
+ throw new Http2ProtocolException("Malformed request: header
field value must not contain CR, LF or NUL")
if (parsed ne null) {
headers += name -> parsed
parsed
@@ -110,6 +118,9 @@ private[http2] final class
HeaderDecompression(masterHeaderParser: HttpHeaderPar
if (truncated) headerListSizeExceeded(streamId)
else push(eventsOut, ParsedHeadersFrame(streamId, endStream,
headers.result(), prioInfo))
} catch {
+ case ex: Http2ProtocolException =>
+ // a malformed header field, rejected by the listener above: fail
with GOAWAY(PROTOCOL_ERROR)
+ failStage(ex)
case _: IOException =>
// this is signalled by the decoder when it failed, we want to
react to this by rendering a GOAWAY frame
fail(eventsOut,
diff --git
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
index 534e0ff63..027c80731 100644
---
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
+++
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/Http2ServerSpec.scala
@@ -98,6 +98,27 @@ class Http2ServerSpec extends Http2SpecWithMaterializer("""
val (_, errorCode) = network.expectGOAWAY(0) // since we have not
processed any stream
errorCode should ===(ErrorCode.COMPRESSION_ERROR)
})
+ "GOAWAY when a header field carries CR, LF or NUL" should {
+ abstract class MalformedHeaderSetup extends TestSetup with
RequestResponseProbes {
+ def expectProtocolError(headerPairs: Seq[(String, String)]): Unit = {
+ user.requestIn.request(1)
+ network.sendHEADERS(1, endStream = true, endHeaders = true,
network.encodeHeaderPairs(headerPairs))
+ val (_, errorCode) = network.expectGOAWAY(0)
+ errorCode should ===(ErrorCode.PROTOCOL_ERROR)
+ // the request, with or without its value truncated, never reaches
the handler
+ user.requestIn.expectNoMessage(100.millis)
+ }
+ def request(extra: (String, String)*): Seq[(String, String)] =
+ Seq(":method" -> "GET", ":scheme" -> "https", ":path" -> "/",
":authority" -> "www.example.com") ++ extra
+ }
+
+ "for a value containing CR LF".inAssertAllStagesStopped(new
MalformedHeaderSetup {
+ expectProtocolError(request("x-a" -> "foo\r\nx-b: bar"))
+ })
+ "for a value containing NUL".inAssertAllStagesStopped(new
MalformedHeaderSetup {
+ expectProtocolError(request("x-a" -> "foo\u0000bar"))
+ })
+ }
"GOAWAY when second request on different stream has invalid headers
frame".inAssertAllStagesStopped(
new SimpleRequestResponseRoundtripSetup {
requestResponseRoundtrip(
diff --git
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
index 57579b500..a97523f31 100644
---
a/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
+++
b/http2-tests/src/test/scala/org/apache/pekko/http/impl/engine/http2/RequestParsingSpec.scala
@@ -23,6 +23,7 @@ import pekko.stream.scaladsl.{ Sink, Source }
import pekko.util.{ ByteString, OptionVal }
import org.scalatest.{ Inside, Inspectors }
import FrameEvent._
+import pekko.http.impl.engine.http2.Http2Compliance.Http2ProtocolException
import pekko.http.impl.engine.http2.hpack.HeaderDecompression
import pekko.http.impl.engine.server.HttpAttributes
import pekko.http.impl.util.PekkoSpecWithMaterializer
@@ -75,6 +76,32 @@ class RequestParsingSpec extends PekkoSpecWithMaterializer
with Inside with Insp
thrown
}
+ "reject a malformed header field" should {
+ // RFC 9113 8.2.1: a field name or value carrying NUL, CR or LF makes
the message malformed
+ def request(extra: (String, String)*): Vector[(String, String)] =
+ Vector(":method" -> "GET", ":scheme" -> "https", ":path" -> "/") ++
extra
+
+ "a header value containing CR LF" in {
+ // the HTTP/1.1 line parser this is handed to stops at the first CRLF
it finds, so without the check the
+ // request was accepted with the value silently truncated to `foo`
+ val thrown = shouldThrowMalformedRequest(parse(request("x-a" ->
"foo\r\nx-b: bar")))
+ thrown shouldBe an[Http2ProtocolException]
+ thrown.getMessage should include("header field value must not contain
CR, LF or NUL")
+ }
+ "a header value containing a bare LF" in {
+ val thrown = shouldThrowMalformedRequest(parse(request("x-a" ->
"foo\nbar")))
+ thrown.getMessage should include("header field value must not contain
CR, LF or NUL")
+ }
+ "a header value containing NUL" in {
+ val thrown = shouldThrowMalformedRequest(parse(request("x-a" ->
"foo\u0000bar")))
+ thrown.getMessage should include("header field value must not contain
CR, LF or NUL")
+ }
+ "a header name containing CR LF" in {
+ val thrown = shouldThrowMalformedRequest(parse(request("x-a\r\nx-b" ->
"v")))
+ thrown.getMessage should include("header field name must not contain
CR, LF or NUL")
+ }
+ }
+
"follow RFC7540" should {
// 8.1.2.1. Pseudo-Header Fields
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]