Skip to content

Commit ae57c2a

Browse files
authored
Validate that connection-specific headers aren't sent. (#95)
Motivation: RFC 7540 forbids the use of connection-specific header fields, except for TE sent with the value "trailers". We should police that. Modifications: - Added extra policing around regular header fields. Result: Better validation
1 parent b4a9ed3 commit ae57c2a

File tree

4 files changed

+315
-14
lines changed

4 files changed

+315
-14
lines changed

Sources/NIOHTTP2/HPACKHeaders+Validation.swift

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ extension HeaderBlockValidator {
8686
for (name, value, _) in block {
8787
let fieldName = try HeaderFieldName(name)
8888
try blockSection.validField(fieldName)
89+
try fieldName.legalHeaderField(value: value)
8990

9091
let thisPseudoHeaderFieldType = try seenPseudoHeaders.seenNewHeaderField(fieldName)
9192

@@ -142,22 +143,28 @@ extension RequestBlockValidator: HeaderBlockValidator {
142143
// This is a bit awkward.
143144
//
144145
// For now we don't support extended-CONNECT, but when we do we'll need to update the logic here.
145-
guard let pseudoHeaderType = pseudoHeaderType else {
146-
// Nothing to do here.
147-
return
148-
}
146+
if let pseudoHeaderType = pseudoHeaderType {
147+
assert(name.fieldType == .pseudoHeaderField)
148+
149+
switch pseudoHeaderType {
150+
case .method:
151+
// This is a method pseudo-header. Check if the value is CONNECT.
152+
self.isConnectRequest = value == "CONNECT"
153+
case .path:
154+
// This is a path pseudo-header. It must not be empty.
155+
if value.utf8.count == 0 {
156+
throw NIOHTTP2Errors.EmptyPathHeader()
157+
}
158+
default:
159+
break
160+
}
161+
} else {
162+
assert(name.fieldType == .regularHeaderField)
149163

150-
switch pseudoHeaderType {
151-
case .method:
152-
// This is a method pseudo-header. Check if the value is CONNECT.
153-
self.isConnectRequest = value == "CONNECT"
154-
case .path:
155-
// This is a path pseudo-header. It must not be empty.
156-
if value.utf8.count == 0 {
157-
throw NIOHTTP2Errors.EmptyPathHeader()
164+
// We want to check that if the TE header field is present, it only contains "trailers".
165+
if name.baseName == "te" && value != "trailers" {
166+
throw NIOHTTP2Errors.ForbiddenHeaderField(name: String(name.baseName), value: value)
158167
}
159-
default:
160-
break
161168
}
162169
}
163170

@@ -249,6 +256,27 @@ extension HeaderFieldName {
249256
throw NIOHTTP2Errors.InvalidHTTP2HeaderFieldName(fieldName)
250257
}
251258
}
259+
260+
func legalHeaderField(value: String) throws {
261+
// RFC 7540 § 8.1.2.2 forbids all connection-specific header fields. A connection-specific header field technically
262+
// is one that is listed in the Connection header, but could also be proxy-connection & transfer-encoding, even though
263+
// those are not usually listed in the Connection header. For defensiveness sake, we forbid those too.
264+
//
265+
// There is one more wrinkle, which is that the client is allowed to send TE: trailers, and forbidden from sending TE
266+
// with anything else. We police that separately, as TE is only defined on requests, so we can avoid checking for it
267+
// on responses and trailers.
268+
guard self.fieldType == .regularHeaderField else {
269+
// Pseudo-headers are never connection-specific.
270+
return
271+
}
272+
273+
switch self.baseName {
274+
case "connection", "transfer-encoding", "proxy-connection":
275+
throw NIOHTTP2Errors.ForbiddenHeaderField(name: String(self.baseName), value: value)
276+
default:
277+
return
278+
}
279+
}
252280
}
253281

254282

Sources/NIOHTTP2/HTTP2Error.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,18 @@ public enum NIOHTTP2Errors {
252252
self.fieldName = fieldName
253253
}
254254
}
255+
256+
/// Connection-specific header fields are forbidden in HTTP/2: this error is raised when one is
257+
/// sent or received.
258+
public struct ForbiddenHeaderField: NIOHTTP2Error {
259+
public var name: String
260+
public var value: String
261+
262+
public init(name: String, value: String) {
263+
self.name = name
264+
self.value = value
265+
}
266+
}
255267
}
256268

257269

Tests/NIOHTTP2Tests/ConnectionStateMachineTests+XCTest.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,15 @@ extension ConnectionStateMachineTests {
113113
("testAllowSimpleConnectRequest", testAllowSimpleConnectRequest),
114114
("testRejectRequestHeadersWithoutAuthorityFieldOnCONNECT", testRejectRequestHeadersWithoutAuthorityFieldOnCONNECT),
115115
("testAllowRequestHeadersWithoutAuthorityFieldOnCONNECTValidationDisabled", testAllowRequestHeadersWithoutAuthorityFieldOnCONNECTValidationDisabled),
116+
("testRejectHeadersWithConnectionHeader", testRejectHeadersWithConnectionHeader),
117+
("testAllowHeadersWithConnectionHeaderWhenValidationDisabled", testAllowHeadersWithConnectionHeaderWhenValidationDisabled),
118+
("testRejectHeadersWithTransferEncodingHeader", testRejectHeadersWithTransferEncodingHeader),
119+
("testAllowHeadersWithTransferEncodingHeaderWhenValidationDisabled", testAllowHeadersWithTransferEncodingHeaderWhenValidationDisabled),
120+
("testRejectHeadersWithProxyConnectionHeader", testRejectHeadersWithProxyConnectionHeader),
121+
("testAllowHeadersWithProxyConnectionHeaderWhenValidationDisabled", testAllowHeadersWithProxyConnectionHeaderWhenValidationDisabled),
122+
("testRejectHeadersWithTEHeaderNotTrailers", testRejectHeadersWithTEHeaderNotTrailers),
123+
("testAllowHeadersWithTEHeaderNotTrailersWhenValidationDisabled", testAllowHeadersWithTEHeaderNotTrailersWhenValidationDisabled),
124+
("testAllowHeadersWithTEHeaderSetToTrailers", testAllowHeadersWithTEHeaderSetToTrailers),
116125
]
117126
}
118127
}

0 commit comments

Comments
 (0)