Skip to content

Commit 41a8b95

Browse files
NIOHTTP2: forbid keep-alive and upgrade connection-specific headers (#552)
RFC 9113 § 8.2.2 enumerates five connection-specific header fields that a conformant HTTP/2 endpoint must treat as malformed: Connection, Proxy-Connection, Keep-Alive, Transfer-Encoding, and Upgrade. HeaderFieldName .legalHeaderField(value:) only rejected three of them, so keep-alive and upgrade passed validation and were translated verbatim into HTTP/1.1 messages by the H2->H1 codecs. This is a conformance gap and a defence-in-depth issue, since downstream HTTP/1.1 parsers may act on those fields independently. Modifications: - Add keep-alive and upgrade to the forbidden connection-specific header switch in legalHeaderField(value:). - Update the surrounding comment to cite RFC 9113 § 8.2.2 and its explicit enumeration in place of the obsoleted RFC 7540 § 8.1.2.2 reference. - Add request- and response-block validation tests covering all five connection-specific fields. Result: All five connection-specific header fields named by RFC 9113 § 8.2.2 are rejected during header-block validation. Resolves #549 Co-authored-by: George Barnett <gbarnett@apple.com>
1 parent 2a044b9 commit 41a8b95

2 files changed

Lines changed: 46 additions & 4 deletions

File tree

Sources/NIOHTTP2/HPACKHeaders+Validation.swift

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,9 +273,10 @@ extension HeaderFieldName {
273273
}
274274

275275
func legalHeaderField(value: String) throws {
276-
// RFC 7540 § 8.1.2.2 forbids all connection-specific header fields. A connection-specific header field technically
277-
// is one that is listed in the Connection header, but could also be proxy-connection & transfer-encoding, even though
278-
// those are not usually listed in the Connection header. For defensiveness sake, we forbid those too.
276+
// RFC 9113 § 8.2.2 (which obsoletes RFC 7540 § 8.1.2.2) forbids all connection-specific header fields, and
277+
// enumerates them explicitly: "Connection, Proxy-Connection, Keep-Alive, Transfer-Encoding, and Upgrade". A
278+
// connection-specific header field technically is one that is listed in the Connection header, but the fields
279+
// named above could also appear independently, so we forbid them all by name.
279280
//
280281
// There is one more wrinkle, which is that the client is allowed to send TE: trailers, and forbidden from sending TE
281282
// with anything else. We police that separately, as TE is only defined on requests, so we can avoid checking for it
@@ -288,7 +289,7 @@ extension HeaderFieldName {
288289
}
289290

290291
switch self.baseName {
291-
case "connection", "transfer-encoding", "proxy-connection":
292+
case "connection", "transfer-encoding", "proxy-connection", "keep-alive", "upgrade":
292293
throw NIOHTTP2Errors.forbiddenHeaderField(name: String(self.baseName), value: value)
293294
default:
294295
return

Tests/NIOHTTP2Tests/HTTP2FramePayloadToHTTP1CodecCRLFTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,47 @@ struct HTTP2FramePayloadToHTTP1CodecCRLFTests {
107107
}
108108
}
109109

110+
// MARK: - Validation tests: connection-specific headers are rejected (RFC 9113 § 8.2.2)
111+
112+
// The five connection-specific header fields that a conformant HTTP/2 endpoint must treat as malformed.
113+
// `te` is policed separately (it is permitted with the single value "trailers"), so it is not in this list.
114+
static let connectionSpecificHeaders = [
115+
"connection", "proxy-connection", "keep-alive", "transfer-encoding", "upgrade",
116+
]
117+
118+
@Test(
119+
"Request validation rejects connection-specific headers",
120+
arguments: connectionSpecificHeaders
121+
)
122+
func requestValidationRejectsConnectionSpecificHeaders(name: String) {
123+
let headers = HPACKHeaders([
124+
(":method", "GET"),
125+
(":path", "/"),
126+
(":scheme", "https"),
127+
(":authority", "example.com"),
128+
(name, "some-value"),
129+
])
130+
let error = #expect(throws: NIOHTTP2Errors.ForbiddenHeaderField.self) {
131+
try headers.validateRequestBlock(supportsExtendedConnect: false)
132+
}
133+
#expect(error?.name == name && error?.value == "some-value")
134+
}
135+
136+
@Test(
137+
"Response validation rejects connection-specific headers",
138+
arguments: connectionSpecificHeaders
139+
)
140+
func responseValidationRejectsConnectionSpecificHeaders(name: String) {
141+
let headers = HPACKHeaders([
142+
(":status", "200"),
143+
(name, "some-value"),
144+
])
145+
let error = #expect(throws: NIOHTTP2Errors.ForbiddenHeaderField.self) {
146+
try headers.validateResponseBlock()
147+
}
148+
#expect(error?.name == name && error?.value == "some-value")
149+
}
150+
110151
// MARK: - Server codec tests: rejects control characters in pseudo-headers
111152

112153
@Test(

0 commit comments

Comments
 (0)