//===----------------------------------------------------------------------===// // // This source file is part of the SwiftNIO open source project // // Copyright (c) 2022 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information // See CONTRIBUTORS.txt for the list of SwiftNIO project authors // // SPDX-License-Identifier: Apache-2.0 // //===----------------------------------------------------------------------===// import Dispatch import NIOCore import NIOEmbedded import NIOHTTP1 import XCTest final class HTTPHeaderValidationTests: XCTestCase { func testEncodingInvalidHeaderFieldNamesInRequests() throws { // The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#fields.values) defines the valid // characters as the following: // // ``` // field-name = token // // token = 1*tchar // // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" // / DIGIT / ALPHA // ; any VCHAR, except delimiters let weirdAllowedFieldName = "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" let channel = EmbeddedChannel() try channel.pipeline.syncOperations.addHTTPClientHandlers() let headers = HTTPHeaders([("Host", "example.com"), (weirdAllowedFieldName, "present")]) let goodRequest = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: headers) let goodRequestBytes = ByteBuffer( string: "GET / HTTP/1.1\r\nHost: example.com\r\n\(weirdAllowedFieldName): present\r\n\r\n" ) XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(goodRequest))) XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.end(nil))) var maybeReceivedBytes: ByteBuffer? XCTAssertNoThrow(maybeReceivedBytes = try channel.readOutbound()) XCTAssertEqual(maybeReceivedBytes, goodRequestBytes) // Now confirm all other bytes are rejected. for byte in UInt8(0)...UInt8(255) { // Skip bytes that we already believe are allowed. if weirdAllowedFieldName.utf8.contains(byte) { continue } let forbiddenFieldName = weirdAllowedFieldName + String(decoding: [byte], as: UTF8.self) let channel = EmbeddedChannel() try channel.pipeline.syncOperations.addHTTPClientHandlers() let headers = HTTPHeaders([("Host", "example.com"), (forbiddenFieldName, "present")]) let badRequest = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: headers) XCTAssertThrowsError( try channel.writeOutbound(HTTPClientRequestPart.head(badRequest)), "Incorrectly tolerated character in header field name: \(String(decoding: [byte], as: UTF8.self))" ) { error in XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) } _ = try? channel.finish() } } func testEncodingInvalidTrailerFieldNamesInRequests() throws { // The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#fields.values) defines the valid // characters as the following: // // ``` // field-name = token // // token = 1*tchar // // tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" // / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" // / DIGIT / ALPHA // ; any VCHAR, except delimiters let weirdAllowedFieldName = "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" let channel = EmbeddedChannel() try channel.pipeline.syncOperations.addHTTPClientHandlers() let headers = HTTPHeaders([("Host", "example.com"), ("Transfer-Encoding", "chunked")]) let goodRequest = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: headers) let goodRequestBytes = ByteBuffer( string: "POST / HTTP/1.1\r\nHost: example.com\r\ntransfer-encoding: chunked\r\n\r\n" ) let goodTrailers = ByteBuffer(string: "0\r\n\(weirdAllowedFieldName): present\r\n\r\n") XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(goodRequest))) XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.end([weirdAllowedFieldName: "present"]))) var maybeRequestHeadBytes: ByteBuffer? var maybeRequestEndBytes: ByteBuffer? XCTAssertNoThrow(maybeRequestHeadBytes = try channel.readOutbound()) XCTAssertNoThrow(maybeRequestEndBytes = try channel.readOutbound()) XCTAssertEqual(maybeRequestHeadBytes, goodRequestBytes) XCTAssertEqual(maybeRequestEndBytes, goodTrailers) // Now confirm all other bytes are rejected. for byte in UInt8(0)...UInt8(255) { // Skip bytes that we already believe are allowed. if weirdAllowedFieldName.utf8.contains(byte) { continue } let forbiddenFieldName = weirdAllowedFieldName + String(decoding: [byte], as: UTF8.self) let channel = EmbeddedChannel() try channel.pipeline.syncOperations.addHTTPClientHandlers() XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(goodRequest))) XCTAssertThrowsError( try channel.writeOutbound(HTTPClientRequestPart.end([forbiddenFieldName: "present"])), "Incorrectly tolerated character in trailer field name: \(String(decoding: [byte], as: UTF8.self))" ) { error in XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) } _ = try? channel.finish() } } func testEncodingInvalidHeaderFieldValuesInRequests() throws { // We reject all ASCII control characters except HTAB and tolerate everything else. let weirdAllowedFieldValue = "!\" \t#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" let channel = EmbeddedChannel() try channel.pipeline.syncOperations.addHTTPClientHandlers() let headers = HTTPHeaders([("Host", "example.com"), ("Weird-Value", weirdAllowedFieldValue)]) let goodRequest = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: headers) let goodRequestBytes = ByteBuffer( string: "GET / HTTP/1.1\r\nHost: example.com\r\nWeird-Value: \(weirdAllowedFieldValue)\r\n\r\n" ) XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(goodRequest))) var maybeBytes: ByteBuffer? XCTAssertNoThrow(maybeBytes = try channel.readOutbound()) XCTAssertEqual(maybeBytes, goodRequestBytes) // Now confirm all other bytes in the ASCII range are rejected. for byte in UInt8(0)..?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" let channel = EmbeddedChannel() try channel.pipeline.syncOperations.addHTTPClientHandlers() let headers = HTTPHeaders([("Host", "example.com"), ("Transfer-Encoding", "chunked")]) let goodRequest = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: headers) let goodRequestBytes = ByteBuffer( string: "POST / HTTP/1.1\r\nHost: example.com\r\ntransfer-encoding: chunked\r\n\r\n" ) let goodTrailers = ByteBuffer(string: "0\r\nWeird-Value: \(weirdAllowedFieldValue)\r\n\r\n") XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(goodRequest))) XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.end(["Weird-Value": weirdAllowedFieldValue]))) var maybeRequestHeadBytes: ByteBuffer? var maybeRequestEndBytes: ByteBuffer? XCTAssertNoThrow(maybeRequestHeadBytes = try channel.readOutbound()) XCTAssertNoThrow(maybeRequestEndBytes = try channel.readOutbound()) XCTAssertEqual(maybeRequestHeadBytes, goodRequestBytes) XCTAssertEqual(maybeRequestEndBytes, goodTrailers) // Now confirm all other bytes in the ASCII range are rejected. for byte in UInt8(0)..?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" let channel = EmbeddedChannel() try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: false) try channel.primeForResponse() let headers = HTTPHeaders([("Content-Length", "0"), ("Weird-Value", weirdAllowedFieldValue)]) let goodResponse = HTTPResponseHead(version: .http1_1, status: .ok, headers: headers) let goodResponseBytes = ByteBuffer( string: "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nWeird-Value: \(weirdAllowedFieldValue)\r\n\r\n" ) XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(goodResponse))) var maybeBytes: ByteBuffer? XCTAssertNoThrow(maybeBytes = try channel.readOutbound()) XCTAssertEqual(maybeBytes, goodResponseBytes) // Now confirm all other bytes in the ASCII range are rejected. for byte in UInt8(0)..?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" let channel = EmbeddedChannel() try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: false) try channel.primeForResponse() let headers = HTTPHeaders([("Transfer-Encoding", "chunked")]) let goodResponse = HTTPResponseHead(version: .http1_1, status: .ok, headers: headers) let goodResponseBytes = ByteBuffer(string: "HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\n") let goodTrailers = ByteBuffer(string: "0\r\nWeird-Value: \(weirdAllowedFieldValue)\r\n\r\n") XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(goodResponse))) XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.end(["Weird-Value": weirdAllowedFieldValue]))) var maybeResponseHeadBytes: ByteBuffer? var maybeResponseEndBytes: ByteBuffer? XCTAssertNoThrow(maybeResponseHeadBytes = try channel.readOutbound()) XCTAssertNoThrow(maybeResponseEndBytes = try channel.readOutbound()) XCTAssertEqual(maybeResponseHeadBytes, goodResponseBytes) XCTAssertEqual(maybeResponseEndBytes, goodTrailers) // Now confirm all other bytes in the ASCII range are rejected. for byte in UInt8(0)..