Skip to content

Commit b4a9ed3

Browse files
Lukasaweissi
authored andcommitted
Forbid empty :path headers. (#94)
Motivation: RFC 7540 does not allow empty :path pseudo-headers. We should police this. Modifications: - Added a check that :path isn't empty. Result: Better RFC 7540 validation.
1 parent 7acb02b commit b4a9ed3

File tree

4 files changed

+48
-3
lines changed

4 files changed

+48
-3
lines changed

Sources/NIOHTTP2/HPACKHeaders+Validation.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,23 @@ extension RequestBlockValidator: HeaderBlockValidator {
142142
// This is a bit awkward.
143143
//
144144
// 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, pseudoHeaderType == .method else {
145+
guard let pseudoHeaderType = pseudoHeaderType else {
146146
// Nothing to do here.
147147
return
148148
}
149149

150-
// This is a method pseudo-header. Check if the value is CONNECT.
151-
self.isConnectRequest = value == "CONNECT"
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()
158+
}
159+
default:
160+
break
161+
}
152162
}
153163

154164
var allowedPseudoHeaderFields: PseudoHeaders {

Sources/NIOHTTP2/HTTP2Error.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ public enum NIOHTTP2Errors {
210210
public init() { }
211211
}
212212

213+
/// A HTTP/2 header block was received with an empty :path header.
214+
public struct EmptyPathHeader: NIOHTTP2Error {
215+
public init() { }
216+
}
217+
213218
/// A :status header was received with an invalid value.
214219
public struct InvalidStatusValue: NIOHTTP2Error {
215220
public var value: String

Tests/NIOHTTP2Tests/ConnectionStateMachineTests+XCTest.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ extension ConnectionStateMachineTests {
9191
("testAllowRequestHeadersWithMissingMethodFieldWhenValidationDisabled", testAllowRequestHeadersWithMissingMethodFieldWhenValidationDisabled),
9292
("testRejectRequestHeadersWithMissingPathField", testRejectRequestHeadersWithMissingPathField),
9393
("testAllowRequestHeadersWithMissingPathFieldWhenValidationDisabled", testAllowRequestHeadersWithMissingPathFieldWhenValidationDisabled),
94+
("testRejectRequestHeadersWithEmptyPathField", testRejectRequestHeadersWithEmptyPathField),
95+
("testAllowRequestHeadersWithEmptyPathFieldWhenValidationDisabled", testAllowRequestHeadersWithEmptyPathFieldWhenValidationDisabled),
9496
("testRejectRequestHeadersWithMissingSchemeField", testRejectRequestHeadersWithMissingSchemeField),
9597
("testAllowRequestHeadersWithMissingSchemeFieldWhenValidationDisabled", testAllowRequestHeadersWithMissingSchemeFieldWhenValidationDisabled),
9698
("testRejectRequestHeadersWithDuplicateMethodField", testRejectRequestHeadersWithDuplicateMethodField),

Tests/NIOHTTP2Tests/ConnectionStateMachineTests.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,34 @@ class ConnectionStateMachineTests: XCTestCase {
19301930
assertSucceeds(self.server.receiveHeaders(streamID: streamOne, headers: headers, isEndStreamSet: true))
19311931
}
19321932

1933+
func testRejectRequestHeadersWithEmptyPathField() {
1934+
let streamOne = HTTP2StreamID(1)
1935+
1936+
self.exchangePreamble()
1937+
1938+
let headers = HPACKHeaders([(":authority", "localhost"), (":scheme", "https"), (":method", "GET"), (":path", ""), ("content-length", "0")])
1939+
1940+
// Request headers must contain non-empty :path.
1941+
assertStreamError(type: .protocolError, self.client.sendHeaders(streamID: streamOne, headers: headers, isEndStreamSet: true))
1942+
assertStreamError(type: .protocolError, self.server.receiveHeaders(streamID: streamOne, headers: headers, isEndStreamSet: true))
1943+
}
1944+
1945+
func testAllowRequestHeadersWithEmptyPathFieldWhenValidationDisabled() {
1946+
let streamOne = HTTP2StreamID(1)
1947+
1948+
// Override the setup with validation disabled.
1949+
self.server = .init(role: .server, headerBlockValidation: .disabled)
1950+
self.client = .init(role: .client, headerBlockValidation: .disabled)
1951+
1952+
self.exchangePreamble()
1953+
1954+
let headers = HPACKHeaders([(":authority", "localhost"), (":scheme", "https"), (":method", "GET"), (":path", ""), ("content-length", "0")])
1955+
1956+
// Request headers must contain non-empty :path.
1957+
assertSucceeds(self.client.sendHeaders(streamID: streamOne, headers: headers, isEndStreamSet: true))
1958+
assertSucceeds(self.server.receiveHeaders(streamID: streamOne, headers: headers, isEndStreamSet: true))
1959+
}
1960+
19331961
func testRejectRequestHeadersWithMissingSchemeField() {
19341962
let streamOne = HTTP2StreamID(1)
19351963

0 commit comments

Comments
 (0)