CVE-2022-3215
Description
CVE-2022-3215: SwiftNIO HTTP/1.1 response injection via CRLF in reflected headers, leading to XSS and cache poisoning, fixed by adding header validation.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CVE-2022-3215: SwiftNIO HTTP/1.1 response injection via CRLF in reflected headers, leading to XSS and cache poisoning, fixed by adding header validation.
Vulnerability
Overview
CVE-2022-3215 is an HTTP response injection vulnerability in NIOHTTP1, the HTTP/1.1 implementation of Apple's SwiftNIO framework. The root cause is the lack of validation on user-supplied input that is reflected into HTTP response headers. An attacker can embed newline characters (CRLF, often URL-encoded) into such reflected headers, thereby injecting arbitrary content into the HTTP response stream. This is a classic HTTP response splitting / CRLF injection flaw [1][3][4].
Exploitation
Mechanism
The attack surface is any HTTP/1.1 server built on SwiftNIO that echoes user input (e.g., from request headers, URL parameters, or request bodies) back into response headers. No special privileges are required; the attacker only needs to send a crafted HTTP request containing encoded newline sequences. Because the framework did not sanitize these header values before writing them onto the wire, the injected newlines would break the response's framing, allowing the attacker to insert new headers, modify the response body, or even terminate the original response early and start a second, attacker-controlled response [1][4].
Impact
Successful exploitation permits an attacker to bypass security headers (e.g., CSP, X-Frame-Options), poison HTTP caches, and stage cross-site scripting (XSS) attacks against users of the vulnerable service. The injected false responses may be interpreted by downstream clients or proxies as the legitimate response to subsequent requests, enabling persistent cache poisoning [1][3][4].
Mitigation
The vulnerability was patched in the SwiftNIO repository by adding a default channel handler that validates HTTP header field values against RFC 9110. All ASCII control characters except for horizontal tab (HTAB) are now either rejected or replaced with linear whitespace (SP). The fix was merged in commit a16e2f5 and included in the advisories (GHSA-7fj7-39wj-c64f). Users are strongly advised to update their SwiftNIO dependency to a patched version. The channel handler is added by default but can be removed by advanced users who perform their own validation [2][4].
- GitHub - apple/swift-nio: Event-driven network application framework for high performance protocol servers & clients, non-blocking.
- Merge pull request from GHSA-7fj7-39wj-c64f · apple/swift-nio@a16e2f5
- NVD - CVE-2022-3215
- Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting') in swift-nio
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/apple/swift-nioSwiftURL | >= 2.41.0, < 2.42.0 | 2.42.0 |
github.com/apple/swift-nioSwiftURL | >= 2.39.0, < 2.39.1 | 2.39.1 |
github.com/apple/swift-nioSwiftURL | < 2.29.1 | 2.29.1 |
Affected products
2- Range: unspecified
Patches
1a16e2f54a25bMerge pull request from GHSA-7fj7-39wj-c64f
13 files changed · +1345 −26
Package.swift+4 −1 modified@@ -65,7 +65,10 @@ var targets: [PackageDescription.Target] = [ .executableTarget(name: "NIOHTTP1Client", dependencies: ["NIOPosix", "NIOCore", "NIOHTTP1", "NIOConcurrencyHelpers"], exclude: ["README.md"]), - .target(name: "CNIOLLHTTP"), + .target( + name: "CNIOLLHTTP", + cSettings: [.define("LLHTTP_STRICT_MODE")] + ), .target(name: "NIOTLS", dependencies: ["NIO", "NIOCore"]), .executableTarget(name: "NIOChatServer", dependencies: ["NIOPosix", "NIOCore", "NIOConcurrencyHelpers"],
Package@swift-5.4.swift+4 −1 modified@@ -65,7 +65,10 @@ var targets: [PackageDescription.Target] = [ .executableTarget(name: "NIOHTTP1Client", dependencies: ["NIOPosix", "NIOCore", "NIOHTTP1", "NIOConcurrencyHelpers"], exclude: ["README.md"]), - .target(name: "CNIOLLHTTP"), + .target( + name: "CNIOLLHTTP", + cSettings: [.define("LLHTTP_STRICT_MODE")] + ), .target(name: "NIOTLS", dependencies: ["NIO", "NIOCore"]), .executableTarget(name: "NIOChatServer", dependencies: ["NIOPosix", "NIOCore", "NIOConcurrencyHelpers"],
Package@swift-5.5.swift+4 −1 modified@@ -65,7 +65,10 @@ var targets: [PackageDescription.Target] = [ .executableTarget(name: "NIOHTTP1Client", dependencies: ["NIOPosix", "NIOCore", "NIOHTTP1", "NIOConcurrencyHelpers"], exclude: ["README.md"]), - .target(name: "CNIOLLHTTP"), + .target( + name: "CNIOLLHTTP", + cSettings: [.define("LLHTTP_STRICT_MODE")] + ), .target(name: "NIOTLS", dependencies: ["NIO", "NIOCore"]), .executableTarget(name: "NIOChatServer", dependencies: ["NIOPosix", "NIOCore", "NIOConcurrencyHelpers"],
Sources/NIOHTTP1/HTTPHeaders+Validation.swift+205 −0 added@@ -0,0 +1,205 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +extension String { + /// Validates a given header field value against the definition in RFC 9110. + /// + /// The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#fields.values) defines the valid + /// characters as the following: + /// + /// ``` + /// field-value = *field-content + /// field-content = field-vchar + /// [ 1*( SP / HTAB / field-vchar ) field-vchar ] + /// field-vchar = VCHAR / obs-text + /// obs-text = %x80-FF + /// ``` + /// + /// Additionally, it makes the following note: + /// + /// "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the + /// varying ways that implementations might parse and interpret those characters; a recipient + /// of CR, LF, or NUL within a field value MUST either reject the message or replace each of + /// those characters with SP before further processing or forwarding of that message. Field + /// values containing other CTL characters are also invalid; however, recipients MAY retain + /// such characters for the sake of robustness when they appear within a safe context (e.g., + /// an application-specific quoted string that will not be processed by any downstream HTTP + /// parser)." + /// + /// As we cannot guarantee the context is safe, this code will reject all ASCII control characters + /// directly _except_ for HTAB, which is explicitly allowed. + fileprivate var isValidHeaderFieldValue: Bool { + let fastResult = self.utf8.withContiguousStorageIfAvailable { ptr in + ptr.allSatisfy { $0.isValidHeaderFieldValueByte } + } + if let fastResult = fastResult { + return fastResult + } else { + return self.utf8._isValidHeaderFieldValue_slowPath + } + } + + /// Validates a given header field name against the definition in RFC 9110. + /// + /// 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 + /// ``` + /// + /// We implement this check directly. + fileprivate var isValidHeaderFieldName: Bool { + let fastResult = self.utf8.withContiguousStorageIfAvailable { ptr in + ptr.allSatisfy { $0.isValidHeaderFieldNameByte } + } + if let fastResult = fastResult { + return fastResult + } else { + return self.utf8._isValidHeaderFieldName_slowPath + } + } +} + +extension String.UTF8View { + /// The equivalent of `String.isValidHeaderFieldName`, but a slow-path when a + /// contiguous UTF8 storage is not available. + /// + /// This is deliberately `@inline(never)`, as slow paths should be forcibly outlined + /// to encourage inlining the fast-path. + @inline(never) + fileprivate var _isValidHeaderFieldName_slowPath: Bool { + self.allSatisfy { $0.isValidHeaderFieldNameByte } + } + + /// The equivalent of `String.isValidHeaderFieldValue`, but a slow-path when a + /// contiguous UTF8 storage is not available. + /// + /// This is deliberately `@inline(never)`, as slow paths should be forcibly outlined + /// to encourage inlining the fast-path. + @inline(never) + fileprivate var _isValidHeaderFieldValue_slowPath: Bool { + self.allSatisfy { $0.isValidHeaderFieldValueByte } + } +} + +extension UInt8 { + /// Validates a byte of a given header field name against the definition in RFC 9110. + /// + /// 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 + /// ``` + /// + /// We implement this check directly. + /// + /// We use inline always here to force the check to be inlined, which it isn't always, leading to less optimal code. + @inline(__always) + fileprivate var isValidHeaderFieldNameByte: Bool { + switch self { + case UInt8(ascii: "0")...UInt8(ascii: "9"), // DIGIT + UInt8(ascii: "a")...UInt8(ascii: "z"), + UInt8(ascii: "A")...UInt8(ascii: "Z"), // ALPHA + UInt8(ascii: "!"), UInt8(ascii: "#"), + UInt8(ascii: "$"), UInt8(ascii: "%"), + UInt8(ascii: "&"), UInt8(ascii: "'"), + UInt8(ascii: "*"), UInt8(ascii: "+"), + UInt8(ascii: "-"), UInt8(ascii: "."), + UInt8(ascii: "^"), UInt8(ascii: "_"), + UInt8(ascii: "`"), UInt8(ascii: "|"), + UInt8(ascii: "~"): + + return true + + default: + return false + } + } + + /// Validates a byte of a given header field value against the definition in RFC 9110. + /// + /// The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#fields.values) defines the valid + /// characters as the following: + /// + /// ``` + /// field-value = *field-content + /// field-content = field-vchar + /// [ 1*( SP / HTAB / field-vchar ) field-vchar ] + /// field-vchar = VCHAR / obs-text + /// obs-text = %x80-FF + /// ``` + /// + /// Additionally, it makes the following note: + /// + /// "Field values containing CR, LF, or NUL characters are invalid and dangerous, due to the + /// varying ways that implementations might parse and interpret those characters; a recipient + /// of CR, LF, or NUL within a field value MUST either reject the message or replace each of + /// those characters with SP before further processing or forwarding of that message. Field + /// values containing other CTL characters are also invalid; however, recipients MAY retain + /// such characters for the sake of robustness when they appear within a safe context (e.g., + /// an application-specific quoted string that will not be processed by any downstream HTTP + /// parser)." + /// + /// As we cannot guarantee the context is safe, this code will reject all ASCII control characters + /// directly _except_ for HTAB, which is explicitly allowed. + /// + /// We use inline always here to force the check to be inlined, which it isn't always, leading to less optimal code. + @inline(__always) + fileprivate var isValidHeaderFieldValueByte: Bool { + switch self { + case UInt8(ascii: "\t"): + // HTAB, explicitly allowed. + return true + case 0...0x1f, 0x7F: + // ASCII control character, forbidden. + return false + default: + // Printable or non-ASCII, allowed. + return true + } + } +} + +extension HTTPHeaders { + /// Whether these HTTPHeaders are valid to send on the wire. + var areValidToSend: Bool { + for (name, value) in self.headers { + if !name.isValidHeaderFieldName { + return false + } + + if !value.isValidHeaderFieldValue { + return false + } + } + + return true + } +}
Sources/NIOHTTP1/HTTPHeaderValidator.swift+97 −0 added@@ -0,0 +1,97 @@ +//===----------------------------------------------------------------------===// +// +// 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 NIOCore + +/// A ChannelHandler to validate that outbound request headers are spec-compliant. +/// +/// The HTTP RFCs constrain the bytes that are validly present within a HTTP/1.1 header block. +/// ``NIOHTTPRequestHeadersValidator`` polices this constraint and ensures that only valid header blocks +/// are emitted on the network. If a header block is invalid, then ``NIOHTTPRequestHeadersValidator`` +/// will send a ``HTTPParserError/invalidHeaderToken``. +/// +/// ``NIOHTTPRequestHeadersValidator`` will also valid that the HTTP trailers are within specification, +/// if they are present. +public final class NIOHTTPRequestHeadersValidator: ChannelOutboundHandler, RemovableChannelHandler { + public typealias OutboundIn = HTTPClientRequestPart + public typealias OutboundOut = HTTPClientRequestPart + + public init() { } + + public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) { + switch self.unwrapOutboundIn(data) { + case .head(let head): + guard head.headers.areValidToSend else { + promise?.fail(HTTPParserError.invalidHeaderToken) + context.fireErrorCaught(HTTPParserError.invalidHeaderToken) + return + } + case .body, .end(.none): + () + case .end(.some(let trailers)): + guard trailers.areValidToSend else { + promise?.fail(HTTPParserError.invalidHeaderToken) + context.fireErrorCaught(HTTPParserError.invalidHeaderToken) + return + } + } + + context.write(data, promise: promise) + } +} + + +/// A ChannelHandler to validate that outbound response headers are spec-compliant. +/// +/// The HTTP RFCs constrain the bytes that are validly present within a HTTP/1.1 header block. +/// ``NIOHTTPResponseHeadersValidator`` polices this constraint and ensures that only valid header blocks +/// are emitted on the network. If a header block is invalid, then ``NIOHTTPResponseHeadersValidator`` +/// will send a ``HTTPParserError/invalidHeaderToken``. +/// +/// ``NIOHTTPResponseHeadersValidator`` will also valid that the HTTP trailers are within specification, +/// if they are present. +public final class NIOHTTPResponseHeadersValidator: ChannelOutboundHandler, RemovableChannelHandler { + public typealias OutboundIn = HTTPServerResponsePart + public typealias OutboundOut = HTTPServerResponsePart + + public init() { } + + public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) { + switch self.unwrapOutboundIn(data) { + case .head(let head): + guard head.headers.areValidToSend else { + promise?.fail(HTTPParserError.invalidHeaderToken) + context.fireErrorCaught(HTTPParserError.invalidHeaderToken) + return + } + case .body, .end(.none): + () + case .end(.some(let trailers)): + guard trailers.areValidToSend else { + promise?.fail(HTTPParserError.invalidHeaderToken) + context.fireErrorCaught(HTTPParserError.invalidHeaderToken) + return + } + } + + context.write(data, promise: promise) + } +} + +#if swift(>=5.6) +@available(*, unavailable) +extension NIOHTTPRequestHeadersValidator: Sendable {} + +@available(*, unavailable) +extension NIOHTTPResponseHeadersValidator: Sendable {} +#endif
Sources/NIOHTTP1/HTTPPipelineSetup.swift+186 −21 modified@@ -123,6 +123,45 @@ extension ChannelPipeline { return future } + + /// Configure a `ChannelPipeline` for use as a HTTP client. + /// + /// - parameters: + /// - position: The position in the `ChannelPipeline` where to add the HTTP client handlers. Defaults to `.last`. + /// - leftOverBytesStrategy: The strategy to use when dealing with leftover bytes after removing the `HTTPDecoder` + /// from the pipeline. + /// - enableOutboundHeaderValidation: Whether the pipeline should confirm that outbound headers are well-formed. + /// Defaults to `true`. + /// - upgrade: Add a ``NIOHTTPClientUpgradeHandler`` to the pipeline, configured for + /// HTTP upgrade. Should be a tuple of an array of ``NIOHTTPClientUpgradeHandler`` and + /// the upgrade completion handler. See the documentation on ``NIOHTTPClientUpgradeHandler`` + /// for more details. + /// - returns: An `EventLoopFuture` that will fire when the pipeline is configured. + public func addHTTPClientHandlers(position: Position = .last, + leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, + enableOutboundHeaderValidation: Bool = true, + withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil) -> EventLoopFuture<Void> { + let future: EventLoopFuture<Void> + + if self.eventLoop.inEventLoop { + let result = Result<Void, Error> { + try self.syncOperations.addHTTPClientHandlers(position: position, + leftOverBytesStrategy: leftOverBytesStrategy, + enableOutboundHeaderValidation: enableOutboundHeaderValidation, + withClientUpgrade: upgrade) + } + future = self.eventLoop.makeCompletedFuture(result) + } else { + future = self.eventLoop.submit { + return try self.syncOperations.addHTTPClientHandlers(position: position, + leftOverBytesStrategy: leftOverBytesStrategy, + enableOutboundHeaderValidation: enableOutboundHeaderValidation, + withClientUpgrade: upgrade) + } + } + + return future + } #if swift(>=5.7) /// Configure a `ChannelPipeline` for use as a HTTP server. @@ -200,27 +239,73 @@ extension ChannelPipeline { ) } #endif + + /// Configure a `ChannelPipeline` for use as a HTTP server. + /// + /// This function knows how to set up all first-party HTTP channel handlers appropriately + /// for server use. It supports the following features: + /// + /// 1. Providing assistance handling clients that pipeline HTTP requests, using the + /// ``HTTPServerPipelineHandler``. + /// 2. Supporting HTTP upgrade, using the ``HTTPServerUpgradeHandler``. + /// 3. Providing assistance handling protocol errors. + /// 4. Validating outbound header fields to protect against response splitting attacks. + /// + /// This method will likely be extended in future with more support for other first-party + /// features. + /// + /// - parameters: + /// - position: Where in the pipeline to add the HTTP server handlers, defaults to `.last`. + /// - pipelining: Whether to provide assistance handling HTTP clients that pipeline + /// their requests. Defaults to `true`. If `false`, users will need to handle + /// clients that pipeline themselves. + /// - upgrade: Whether to add a `HTTPServerUpgradeHandler` to the pipeline, configured for + /// HTTP upgrade. Defaults to `nil`, which will not add the handler to the pipeline. If + /// provided should be a tuple of an array of `HTTPServerProtocolUpgrader` and the upgrade + /// completion handler. See the documentation on `HTTPServerUpgradeHandler` for more + /// details. + /// - errorHandling: Whether to provide assistance handling protocol errors (e.g. + /// failure to parse the HTTP request) by sending 400 errors. Defaults to `true`. + /// - headerValidation: Whether to validate outbound request headers to confirm that they meet + /// spec compliance. Defaults to `true`. + /// - returns: An `EventLoopFuture` that will fire when the pipeline is configured. + public func configureHTTPServerPipeline(position: ChannelPipeline.Position = .last, + withPipeliningAssistance pipelining: Bool = true, + withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, + withErrorHandling errorHandling: Bool = true, + withOutboundHeaderValidation headerValidation: Bool = true) -> EventLoopFuture<Void> { + self._configureHTTPServerPipeline( + position: position, + withPipeliningAssistance: pipelining, + withServerUpgrade: upgrade, + withErrorHandling: errorHandling, + withOutboundHeaderValidation: headerValidation + ) + } private func _configureHTTPServerPipeline(position: ChannelPipeline.Position = .last, withPipeliningAssistance pipelining: Bool = true, withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, - withErrorHandling errorHandling: Bool = true) -> EventLoopFuture<Void> { + withErrorHandling errorHandling: Bool = true, + withOutboundHeaderValidation headerValidation: Bool = true) -> EventLoopFuture<Void> { let future: EventLoopFuture<Void> if self.eventLoop.inEventLoop { let result = Result<Void, Error> { try self.syncOperations.configureHTTPServerPipeline(position: position, withPipeliningAssistance: pipelining, withServerUpgrade: upgrade, - withErrorHandling: errorHandling) + withErrorHandling: errorHandling, + withOutboundHeaderValidation: headerValidation) } future = self.eventLoop.makeCompletedFuture(result) } else { future = self.eventLoop.submit { try self.syncOperations.configureHTTPServerPipeline(position: position, withPipeliningAssistance: pipelining, withServerUpgrade: upgrade, - withErrorHandling: errorHandling) + withErrorHandling: errorHandling, + withOutboundHeaderValidation: headerValidation) } } @@ -275,17 +360,40 @@ extension ChannelPipeline.SynchronousOperations { ) } #endif - - private func _addHTTPClientHandlers(position: ChannelPipeline.Position = .last, + + /// Configure a `ChannelPipeline` for use as a HTTP client. + /// + /// - important: This **must** be called on the Channel's event loop. + /// - parameters: + /// - position: The position in the `ChannelPipeline` where to add the HTTP client handlers. Defaults to `.last`. + /// - leftOverBytesStrategy: The strategy to use when dealing with leftover bytes after removing the `HTTPDecoder` + /// from the pipeline. + /// - upgrade: Add a ``NIOHTTPClientUpgradeHandler`` to the pipeline, configured for + /// HTTP upgrade. Should be a tuple of an array of ``NIOHTTPClientProtocolUpgrader`` and + /// the upgrade completion handler. See the documentation on ``NIOHTTPClientUpgradeHandler`` + /// for more details. + /// - throws: If the pipeline could not be configured. + public func addHTTPClientHandlers(position: ChannelPipeline.Position = .last, leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, + enableOutboundHeaderValidation: Bool = true, withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil) throws { - // Why two separate functions? When creating the array of handlers to add to the pipeline, when we don't have - // an upgrade handler -- i.e. just an array literal -- the compiler is able to promote the array to the stack - // which saves an allocation. That's not the case when the upgrade handler is present. - if let upgrade = upgrade { - try self._addHTTPClientHandlers(position: position, - leftOverBytesStrategy: leftOverBytesStrategy, - withClientUpgrade: upgrade) + try self._addHTTPClientHandlers(position: position, + leftOverBytesStrategy: leftOverBytesStrategy, + enableOutboundHeaderValidation: enableOutboundHeaderValidation, + withClientUpgrade: upgrade) + } + + private func _addHTTPClientHandlers(position: ChannelPipeline.Position = .last, + leftOverBytesStrategy: RemoveAfterUpgradeStrategy = .dropBytes, + enableOutboundHeaderValidation: Bool = true, + withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration? = nil) throws { + // Why two separate functions? With the fast-path (no upgrader, yes header validator) we can promote the Array of handlers + // to the stack and skip an allocation. + if upgrade != nil || enableOutboundHeaderValidation != true { + try self._addHTTPClientHandlersFallback(position: position, + leftOverBytesStrategy: leftOverBytesStrategy, + enableOutboundHeaderValidation: enableOutboundHeaderValidation, + withClientUpgrade: upgrade) } else { try self._addHTTPClientHandlers(position: position, leftOverBytesStrategy: leftOverBytesStrategy) @@ -297,22 +405,30 @@ extension ChannelPipeline.SynchronousOperations { self.eventLoop.assertInEventLoop() let requestEncoder = HTTPRequestEncoder() let responseDecoder = HTTPResponseDecoder(leftOverBytesStrategy: leftOverBytesStrategy) - let handlers: [ChannelHandler] = [requestEncoder, ByteToMessageHandler(responseDecoder)] + let requestHeaderValidator = NIOHTTPRequestHeadersValidator() + let handlers: [ChannelHandler] = [requestEncoder, ByteToMessageHandler(responseDecoder), requestHeaderValidator] try self.addHandlers(handlers, position: position) } - private func _addHTTPClientHandlers(position: ChannelPipeline.Position, - leftOverBytesStrategy: RemoveAfterUpgradeStrategy, - withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration) throws { + private func _addHTTPClientHandlersFallback(position: ChannelPipeline.Position, + leftOverBytesStrategy: RemoveAfterUpgradeStrategy, + enableOutboundHeaderValidation: Bool, + withClientUpgrade upgrade: NIOHTTPClientUpgradeConfiguration?) throws { self.eventLoop.assertInEventLoop() let requestEncoder = HTTPRequestEncoder() let responseDecoder = HTTPResponseDecoder(leftOverBytesStrategy: leftOverBytesStrategy) var handlers: [RemovableChannelHandler] = [requestEncoder, ByteToMessageHandler(responseDecoder)] - let upgrader = NIOHTTPClientUpgradeHandler(upgraders: upgrade.upgraders, - httpHandlers: handlers, - upgradeCompletionHandler: upgrade.completionHandler) - handlers.append(upgrader) + if enableOutboundHeaderValidation { + handlers.append(NIOHTTPRequestHeadersValidator()) + } + + if let upgrade = upgrade { + let upgrader = NIOHTTPClientUpgradeHandler(upgraders: upgrade.upgraders, + httpHandlers: handlers, + upgradeCompletionHandler: upgrade.completionHandler) + handlers.append(upgrader) + } try self.addHandlers(handlers, position: position) } @@ -394,11 +510,56 @@ extension ChannelPipeline.SynchronousOperations { ) } #endif + + /// Configure a `ChannelPipeline` for use as a HTTP server. + /// + /// This function knows how to set up all first-party HTTP channel handlers appropriately + /// for server use. It supports the following features: + /// + /// 1. Providing assistance handling clients that pipeline HTTP requests, using the + /// `HTTPServerPipelineHandler`. + /// 2. Supporting HTTP upgrade, using the `HTTPServerUpgradeHandler`. + /// 3. Providing assistance handling protocol errors. + /// 4. Validating outbound header fields to protect against response splitting attacks. + /// + /// This method will likely be extended in future with more support for other first-party + /// features. + /// + /// - important: This **must** be called on the Channel's event loop. + /// - parameters: + /// - position: Where in the pipeline to add the HTTP server handlers, defaults to `.last`. + /// - pipelining: Whether to provide assistance handling HTTP clients that pipeline + /// their requests. Defaults to `true`. If `false`, users will need to handle + /// clients that pipeline themselves. + /// - upgrade: Whether to add a `HTTPServerUpgradeHandler` to the pipeline, configured for + /// HTTP upgrade. Defaults to `nil`, which will not add the handler to the pipeline. If + /// provided should be a tuple of an array of `HTTPServerProtocolUpgrader` and the upgrade + /// completion handler. See the documentation on `HTTPServerUpgradeHandler` for more + /// details. + /// - errorHandling: Whether to provide assistance handling protocol errors (e.g. + /// failure to parse the HTTP request) by sending 400 errors. Defaults to `true`. + /// - headerValidation: Whether to validate outbound request headers to confirm that they meet + /// spec compliance. Defaults to `true`. + /// - throws: If the pipeline could not be configured. + public func configureHTTPServerPipeline(position: ChannelPipeline.Position = .last, + withPipeliningAssistance pipelining: Bool = true, + withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, + withErrorHandling errorHandling: Bool = true, + withOutboundHeaderValidation headerValidation: Bool = true) throws { + try self._configureHTTPServerPipeline( + position: position, + withPipeliningAssistance: pipelining, + withServerUpgrade: upgrade, + withErrorHandling: errorHandling, + withOutboundHeaderValidation: headerValidation + ) + } private func _configureHTTPServerPipeline(position: ChannelPipeline.Position = .last, withPipeliningAssistance pipelining: Bool = true, withServerUpgrade upgrade: NIOHTTPServerUpgradeConfiguration? = nil, - withErrorHandling errorHandling: Bool = true) throws { + withErrorHandling errorHandling: Bool = true, + withOutboundHeaderValidation headerValidation: Bool = true) throws { self.eventLoop.assertInEventLoop() let responseEncoder = HTTPResponseEncoder() @@ -410,6 +571,10 @@ extension ChannelPipeline.SynchronousOperations { handlers.append(HTTPServerPipelineHandler()) } + if headerValidation { + handlers.append(NIOHTTPResponseHeadersValidator()) + } + if errorHandling { handlers.append(HTTPServerProtocolErrorHandler()) }
Tests/LinuxMain.swift+1 −0 modified@@ -84,6 +84,7 @@ class LinuxMainRunnerImpl: LinuxMainRunner { testCase(HTTPClientUpgradeTestCase.allTests), testCase(HTTPDecoderLengthTest.allTests), testCase(HTTPDecoderTest.allTests), + testCase(HTTPHeaderValidationTests.allTests), testCase(HTTPHeadersTest.allTests), testCase(HTTPRequestEncoderTests.allTests), testCase(HTTPResponseEncoderTests.allTests),
Tests/NIOHTTP1Tests/HTTPDecoderTest.swift+229 −0 modified@@ -1026,4 +1026,233 @@ class HTTPDecoderTest: XCTestCase { XCTAssertEqual(head.method, .RAW(value: query)) } } + + func testDecodingInvalidHeaderFieldNames() 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" + + XCTAssertNoThrow(try self.channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + let goodRequest = ByteBuffer(string: "GET / HTTP/1.1\r\nHost: example.com\r\n\(weirdAllowedFieldName): present\r\n\r\n") + + XCTAssertNoThrow(try self.channel.writeInbound(goodRequest)) + + var maybeHead: HTTPServerRequestPart? + var maybeEnd: HTTPServerRequestPart? + + XCTAssertNoThrow(maybeHead = try self.channel.readInbound()) + XCTAssertNoThrow(maybeEnd = try self.channel.readInbound()) + guard case .some(.head(let head)) = maybeHead, case .some(.end) = maybeEnd else { + XCTFail("didn't receive head & end") + return + } + XCTAssertEqual(head.headers[weirdAllowedFieldName], ["present"]) + + // Now confirm all other bytes are rejected. + for byte in UInt8(0)...UInt8(255) { + // Skip bytes that we already believe are allowed, or that will affect the parse. + if weirdAllowedFieldName.utf8.contains(byte) || byte == UInt8(ascii: ":") { + continue + } + let forbiddenFieldName = weirdAllowedFieldName + String(decoding: [byte], as: UTF8.self) + let channel = EmbeddedChannel(handler: ByteToMessageHandler(HTTPRequestDecoder())) + let badRequest = ByteBuffer(string: "GET / HTTP/1.1\r\nHost: example.com\r\n\(forbiddenFieldName): present\r\n\r\n") + + XCTAssertThrowsError( + try channel.writeInbound(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 testDecodingInvalidTrailerFieldNames() 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 request = ByteBuffer(string: "POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n") + + XCTAssertNoThrow(try self.channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + let goodTrailers = ByteBuffer(string: "\(weirdAllowedFieldName): present\r\n\r\n") + + XCTAssertNoThrow(try self.channel.writeInbound(request)) + XCTAssertNoThrow(try self.channel.writeInbound(goodTrailers)) + + var maybeHead: HTTPServerRequestPart? + var maybeEnd: HTTPServerRequestPart? + + XCTAssertNoThrow(maybeHead = try self.channel.readInbound()) + XCTAssertNoThrow(maybeEnd = try self.channel.readInbound()) + guard case .some(.head) = maybeHead, case .some(.end(.some(let trailers))) = maybeEnd else { + XCTFail("didn't receive head & end") + return + } + XCTAssertEqual(trailers[weirdAllowedFieldName], ["present"]) + + // Now confirm all other bytes are rejected. + for byte in UInt8(0)...UInt8(255) { + // Skip bytes that we already believe are allowed, or that will affect the parse. + if weirdAllowedFieldName.utf8.contains(byte) || byte == UInt8(ascii: ":") { + continue + } + let forbiddenFieldName = weirdAllowedFieldName + String(decoding: [byte], as: UTF8.self) + let channel = EmbeddedChannel(handler: ByteToMessageHandler(HTTPRequestDecoder())) + let badTrailers = ByteBuffer(string: "\(forbiddenFieldName): present\r\n\r\n") + + XCTAssertNoThrow(try channel.writeInbound(request)) + + XCTAssertThrowsError( + try channel.writeInbound(badTrailers), + "Incorrectly tolerated character in trailer field name: \(String(decoding: [byte], as: UTF8.self))" + ) { error in + XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) + } + _ = try? channel.finish() + } + } + + func testDecodingInvalidHeaderFieldValues() throws { + // We reject all ASCII control characters except HTAB and tolerate everything else. + let weirdAllowedFieldValue = "!\" \t#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + + XCTAssertNoThrow(try self.channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + let goodRequest = ByteBuffer(string: "GET / HTTP/1.1\r\nHost: example.com\r\nWeird-Field: \(weirdAllowedFieldValue)\r\n\r\n") + + XCTAssertNoThrow(try self.channel.writeInbound(goodRequest)) + + var maybeHead: HTTPServerRequestPart? + var maybeEnd: HTTPServerRequestPart? + + XCTAssertNoThrow(maybeHead = try self.channel.readInbound()) + XCTAssertNoThrow(maybeEnd = try self.channel.readInbound()) + guard case .some(.head(let head)) = maybeHead, case .some(.end) = maybeEnd else { + XCTFail("didn't receive head & end") + return + } + XCTAssertEqual(head.headers["Weird-Field"], [weirdAllowedFieldValue]) + + // Now confirm all other bytes in the ASCII range are rejected. + for byte in UInt8(0)..<UInt8(128) { + // Skip bytes that we already believe are allowed, or that will affect the parse. + if weirdAllowedFieldValue.utf8.contains(byte) || byte == UInt8(ascii: "\r") || byte == UInt8(ascii: "\n") { + continue + } + let forbiddenFieldValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + let channel = EmbeddedChannel(handler: ByteToMessageHandler(HTTPRequestDecoder())) + let badRequest = ByteBuffer(string: "GET / HTTP/1.1\r\nHost: example.com\r\nWeird-Field: \(forbiddenFieldValue)\r\n\r\n") + + XCTAssertThrowsError( + try channel.writeInbound(badRequest), + "Incorrectly tolerated character in header field value: \(String(decoding: [byte], as: UTF8.self))" + ) { error in + XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) + } + _ = try? channel.finish() + } + + // All the bytes outside the ASCII range are fine though. + for byte in UInt8(128)...UInt8(255) { + let evenWeirderAllowedValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + let channel = EmbeddedChannel(handler: ByteToMessageHandler(HTTPRequestDecoder())) + let weirdGoodRequest = ByteBuffer(string: "GET / HTTP/1.1\r\nHost: example.com\r\nWeird-Field: \(evenWeirderAllowedValue)\r\n\r\n") + + XCTAssertNoThrow(try channel.writeInbound(weirdGoodRequest)) + XCTAssertNoThrow(maybeHead = try channel.readInbound()) + XCTAssertNoThrow(maybeEnd = try channel.readInbound()) + guard case .some(.head(let head)) = maybeHead, case .some(.end) = maybeEnd else { + XCTFail("didn't receive head & end for \(byte)") + return + } + XCTAssertEqual(head.headers["Weird-Field"], [evenWeirderAllowedValue]) + + _ = try? channel.finish() + } + } + + func testDecodingInvalidTrailerFieldValues() throws { + // We reject all ASCII control characters except HTAB and tolerate everything else. + let weirdAllowedFieldValue = "!\" \t#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + + let request = ByteBuffer(string: "POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n0\r\n") + + XCTAssertNoThrow(try self.channel.pipeline.addHandler(ByteToMessageHandler(HTTPRequestDecoder())).wait()) + let goodTrailers = ByteBuffer(string: "Weird-Field: \(weirdAllowedFieldValue)\r\n\r\n") + + XCTAssertNoThrow(try self.channel.writeInbound(request)) + XCTAssertNoThrow(try self.channel.writeInbound(goodTrailers)) + + var maybeHead: HTTPServerRequestPart? + var maybeEnd: HTTPServerRequestPart? + + XCTAssertNoThrow(maybeHead = try self.channel.readInbound()) + XCTAssertNoThrow(maybeEnd = try self.channel.readInbound()) + guard case .some(.head) = maybeHead, case .some(.end(.some(let trailers))) = maybeEnd else { + XCTFail("didn't receive head & end") + return + } + XCTAssertEqual(trailers["Weird-Field"], [weirdAllowedFieldValue]) + + // Now confirm all other bytes in the ASCII range are rejected. + for byte in UInt8(0)..<UInt8(128) { + // Skip bytes that we already believe are allowed, or that will affect the parse. + if weirdAllowedFieldValue.utf8.contains(byte) || byte == UInt8(ascii: "\r") || byte == UInt8(ascii: "\n") { + continue + } + let forbiddenFieldValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + let channel = EmbeddedChannel(handler: ByteToMessageHandler(HTTPRequestDecoder())) + let badTrailers = ByteBuffer(string: "Weird-Field: \(forbiddenFieldValue)\r\n\r\n") + + XCTAssertNoThrow(try channel.writeInbound(request)) + + XCTAssertThrowsError( + try channel.writeInbound(badTrailers), + "Incorrectly tolerated character in trailer field value: \(String(decoding: [byte], as: UTF8.self))" + ) { error in + XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) + } + _ = try? channel.finish() + } + + // All the bytes outside the ASCII range are fine though. + for byte in UInt8(128)...UInt8(255) { + let evenWeirderAllowedValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + let channel = EmbeddedChannel(handler: ByteToMessageHandler(HTTPRequestDecoder())) + let weirdGoodTrailers = ByteBuffer(string: "Weird-Field: \(evenWeirderAllowedValue)\r\n\r\n") + + XCTAssertNoThrow(try channel.writeInbound(request)) + XCTAssertNoThrow(try channel.writeInbound(weirdGoodTrailers)) + XCTAssertNoThrow(maybeHead = try channel.readInbound()) + XCTAssertNoThrow(maybeEnd = try channel.readInbound()) + guard case .some(.head) = maybeHead, case .some(.end(.some(let trailers))) = maybeEnd else { + XCTFail("didn't receive head & end for \(byte)") + return + } + XCTAssertEqual(trailers["Weird-Field"], [evenWeirderAllowedValue]) + + _ = try? channel.finish() + } + } }
Tests/NIOHTTP1Tests/HTTPDecoderTest+XCTest.swift+4 −0 modified@@ -62,6 +62,10 @@ extension HTTPDecoderTest { ("testDecodingLongHeaderFieldValues", testDecodingLongHeaderFieldValues), ("testDecodingLongURLs", testDecodingLongURLs), ("testDecodingRTSPQueries", testDecodingRTSPQueries), + ("testDecodingInvalidHeaderFieldNames", testDecodingInvalidHeaderFieldNames), + ("testDecodingInvalidTrailerFieldNames", testDecodingInvalidTrailerFieldNames), + ("testDecodingInvalidHeaderFieldValues", testDecodingInvalidHeaderFieldValues), + ("testDecodingInvalidTrailerFieldValues", testDecodingInvalidTrailerFieldValues), ] } }
Tests/NIOHTTP1Tests/HTTPHeaderValidationTests.swift+566 −0 added@@ -0,0 +1,566 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +import Dispatch +import NIOCore +import NIOEmbedded +import NIOHTTP1 + +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)..<UInt8(128) { + // Skip bytes that we already believe are allowed. + if weirdAllowedFieldValue.utf8.contains(byte) { + continue + } + let forbiddenFieldValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + + let channel = EmbeddedChannel() + try channel.pipeline.syncOperations.addHTTPClientHandlers() + + let headers = HTTPHeaders([("Host", "example.com"), ("Weird-Value", forbiddenFieldValue)]) + let badRequest = HTTPRequestHead(version: .http1_1, method: .GET, uri: "/", headers: headers) + + XCTAssertThrowsError( + try channel.writeOutbound(HTTPClientRequestPart.head(badRequest)), + "Incorrectly tolerated character in header field value: \(String(decoding: [byte], as: UTF8.self))" + ) { error in + XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) + } + _ = try? channel.finish() + } + + // All the bytes outside the ASCII range are fine though. + for byte in UInt8(128)...UInt8(255) { + let evenWeirderAllowedValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + + let channel = EmbeddedChannel() + try channel.pipeline.syncOperations.addHTTPClientHandlers() + + let headers = HTTPHeaders([("Host", "example.com"), ("Weird-Value", evenWeirderAllowedValue)]) + 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: \(evenWeirderAllowedValue)\r\n\r\n") + + XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(goodRequest))) + + var maybeBytes: ByteBuffer? + + XCTAssertNoThrow(maybeBytes = try channel.readOutbound()) + XCTAssertEqual(maybeBytes, goodRequestBytes) + + _ = try? channel.finish() + } + } + + func testEncodingInvalidTrailerFieldValuesInRequests() 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"), ("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)..<UInt8(128) { + // Skip bytes that we already believe are allowed. + if weirdAllowedFieldValue.utf8.contains(byte) { + continue + } + let forbiddenFieldValue = weirdAllowedFieldValue + 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(["Weird-Value": forbiddenFieldValue])), + "Incorrectly tolerated character in trailer field value: \(String(decoding: [byte], as: UTF8.self))" + ) { error in + XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) + } + _ = try? channel.finish() + } + + // All the bytes outside the ASCII range are fine though. + for byte in UInt8(128)...UInt8(255) { + let evenWeirderAllowedValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + + let channel = EmbeddedChannel() + try channel.pipeline.syncOperations.addHTTPClientHandlers() + + let weirdGoodTrailers = ByteBuffer(string: "0\r\nWeird-Value: \(evenWeirderAllowedValue)\r\n\r\n") + + XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(goodRequest))) + XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.end(["Weird-Value": evenWeirderAllowedValue]))) + XCTAssertNoThrow(maybeRequestHeadBytes = try channel.readOutbound()) + XCTAssertNoThrow(maybeRequestEndBytes = try channel.readOutbound()) + XCTAssertEqual(maybeRequestHeadBytes, goodRequestBytes) + XCTAssertEqual(maybeRequestEndBytes, weirdGoodTrailers) + + _ = try? channel.finish() + } + } + + func testEncodingInvalidHeaderFieldNamesInResponses() 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.configureHTTPServerPipeline(withErrorHandling: false) + try channel.primeForResponse() + + let headers = HTTPHeaders([("Content-Length", "0"), (weirdAllowedFieldName, "present")]) + let goodResponse = HTTPResponseHead(version: .http1_1, status: .ok, headers: headers) + let goodResponseBytes = ByteBuffer(string: "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\(weirdAllowedFieldName): present\r\n\r\n") + + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(goodResponse))) + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.end(nil))) + + var maybeReceivedBytes: ByteBuffer? + + XCTAssertNoThrow(maybeReceivedBytes = try channel.readOutbound()) + XCTAssertEqual(maybeReceivedBytes, goodResponseBytes) + + // 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.configureHTTPServerPipeline(withErrorHandling: false) + try channel.primeForResponse() + + let headers = HTTPHeaders([("Content-Length", "0"), (forbiddenFieldName, "present")]) + let badResponse = HTTPResponseHead(version: .http1_1, status: .ok, headers: headers) + + XCTAssertThrowsError( + try channel.writeOutbound(HTTPServerResponsePart.head(badResponse)), + "Incorrectly tolerated character in header field name: \(String(decoding: [byte], as: UTF8.self))" + ) { error in + XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) + } + _ = try? channel.finish() + } + } + + func testEncodingInvalidTrailerFieldNamesInResponses() 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.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\n\(weirdAllowedFieldName): present\r\n\r\n") + + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(goodResponse))) + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.end([weirdAllowedFieldName: "present"]))) + + var maybeRequestHeadBytes: ByteBuffer? + var maybeRequestEndBytes: ByteBuffer? + + XCTAssertNoThrow(maybeRequestHeadBytes = try channel.readOutbound()) + XCTAssertNoThrow(maybeRequestEndBytes = try channel.readOutbound()) + XCTAssertEqual(maybeRequestHeadBytes, goodResponseBytes) + 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.configureHTTPServerPipeline(withErrorHandling: false) + try channel.primeForResponse() + + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(goodResponse))) + + XCTAssertThrowsError( + try channel.writeOutbound(HTTPServerResponsePart.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 testEncodingInvalidHeaderFieldValuesInResponses() 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.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)..<UInt8(128) { + // Skip bytes that we already believe are allowed. + if weirdAllowedFieldValue.utf8.contains(byte) { + continue + } + let forbiddenFieldValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + + let channel = EmbeddedChannel() + try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: false) + try channel.primeForResponse() + + let headers = HTTPHeaders([("Content-Length", "0"), ("Weird-Value", forbiddenFieldValue)]) + let badResponse = HTTPResponseHead(version: .http1_1, status: .ok, headers: headers) + + XCTAssertThrowsError( + try channel.writeOutbound(HTTPServerResponsePart.head(badResponse)), + "Incorrectly tolerated character in header field value: \(String(decoding: [byte], as: UTF8.self))" + ) { error in + XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) + } + _ = try? channel.finish() + } + + // All the bytes outside the ASCII range are fine though. + for byte in UInt8(128)...UInt8(255) { + let evenWeirderAllowedValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + + let channel = EmbeddedChannel() + try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: false) + try channel.primeForResponse() + + let headers = HTTPHeaders([("Content-Length", "0"), ("Weird-Value", evenWeirderAllowedValue)]) + 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: \(evenWeirderAllowedValue)\r\n\r\n") + + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(goodResponse))) + + var maybeBytes: ByteBuffer? + + XCTAssertNoThrow(maybeBytes = try channel.readOutbound()) + XCTAssertEqual(maybeBytes, goodResponseBytes) + + _ = try? channel.finish() + } + } + + func testEncodingInvalidTrailerFieldValuesInResponses() 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.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)..<UInt8(128) { + // Skip bytes that we already believe are allowed. + if weirdAllowedFieldValue.utf8.contains(byte) { + continue + } + let forbiddenFieldValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + + let channel = EmbeddedChannel() + try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: false) + try channel.primeForResponse() + + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(goodResponse))) + + XCTAssertThrowsError( + try channel.writeOutbound(HTTPServerResponsePart.end(["Weird-Value": forbiddenFieldValue])), + "Incorrectly tolerated character in trailer field value: \(String(decoding: [byte], as: UTF8.self))" + ) { error in + XCTAssertEqual(error as? HTTPParserError, .invalidHeaderToken) + } + _ = try? channel.finish() + } + + // All the bytes outside the ASCII range are fine though. + for byte in UInt8(128)...UInt8(255) { + let evenWeirderAllowedValue = weirdAllowedFieldValue + String(decoding: [byte], as: UTF8.self) + + let channel = EmbeddedChannel() + try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: false) + try channel.primeForResponse() + + let weirdGoodTrailers = ByteBuffer(string: "0\r\nWeird-Value: \(evenWeirderAllowedValue)\r\n\r\n") + + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(goodResponse))) + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.end(["Weird-Value": evenWeirderAllowedValue]))) + XCTAssertNoThrow(maybeResponseHeadBytes = try channel.readOutbound()) + XCTAssertNoThrow(maybeResponseEndBytes = try channel.readOutbound()) + XCTAssertEqual(maybeResponseHeadBytes, goodResponseBytes) + XCTAssertEqual(maybeResponseEndBytes, weirdGoodTrailers) + + _ = try? channel.finish() + } + } + + func testDisablingValidationClientSide() throws { + let invalidHeaderName = "HeaderNameWith\"Quote" + let invalidHeaderValue = "HeaderValueWith\rCR" + + let channel = EmbeddedChannel() + try channel.pipeline.syncOperations.addHTTPClientHandlers(enableOutboundHeaderValidation: false) + + let headers = HTTPHeaders([("Host", "example.com"), ("Transfer-Encoding", "chunked"), (invalidHeaderName, invalidHeaderValue)]) + let toleratedRequest = HTTPRequestHead(version: .http1_1, method: .POST, uri: "/", headers: headers) + let toleratedRequestBytes = ByteBuffer(string: "POST / HTTP/1.1\r\nHost: example.com\r\n\(invalidHeaderName): \(invalidHeaderValue)\r\ntransfer-encoding: chunked\r\n\r\n") + let toleratedTrailerBytes = ByteBuffer(string: "0\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\(invalidHeaderName): \(invalidHeaderValue)\r\n\r\n") + + XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.head(toleratedRequest))) + XCTAssertNoThrow(try channel.writeOutbound(HTTPClientRequestPart.end(headers))) + + var maybeReceivedHeadBytes: ByteBuffer? + var maybeReceivedTrailerBytes: ByteBuffer? + + XCTAssertNoThrow(maybeReceivedHeadBytes = try channel.readOutbound()) + XCTAssertNoThrow(maybeReceivedTrailerBytes = try channel.readOutbound()) + XCTAssertEqual(maybeReceivedHeadBytes, toleratedRequestBytes) + XCTAssertEqual(maybeReceivedTrailerBytes, toleratedTrailerBytes) + } + + func testDisablingValidationServerSide() throws { + let invalidHeaderName = "HeaderNameWith\"Quote" + let invalidHeaderValue = "HeaderValueWith\rCR" + + let channel = EmbeddedChannel() + try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: false, withOutboundHeaderValidation: false) + try channel.primeForResponse() + + let headers = HTTPHeaders([("Host", "example.com"), ("Transfer-Encoding", "chunked"), (invalidHeaderName, invalidHeaderValue)]) + let toleratedRequest = HTTPResponseHead(version: .http1_1, status: .ok, headers: headers) + let toleratedRequestBytes = ByteBuffer(string: "HTTP/1.1 200 OK\r\nHost: example.com\r\n\(invalidHeaderName): \(invalidHeaderValue)\r\ntransfer-encoding: chunked\r\n\r\n") + let toleratedTrailerBytes = ByteBuffer(string: "0\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\(invalidHeaderName): \(invalidHeaderValue)\r\n\r\n") + + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.head(toleratedRequest))) + XCTAssertNoThrow(try channel.writeOutbound(HTTPServerResponsePart.end(headers))) + + var maybeReceivedHeadBytes: ByteBuffer? + var maybeReceivedTrailerBytes: ByteBuffer? + + XCTAssertNoThrow(maybeReceivedHeadBytes = try channel.readOutbound()) + XCTAssertNoThrow(maybeReceivedTrailerBytes = try channel.readOutbound()) + XCTAssertEqual(maybeReceivedHeadBytes, toleratedRequestBytes) + XCTAssertEqual(maybeReceivedTrailerBytes, toleratedTrailerBytes) + } +} + +extension EmbeddedChannel { + fileprivate func primeForResponse() throws { + let request = ByteBuffer(string: "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + try self.writeInbound(request) + } +}
Tests/NIOHTTP1Tests/HTTPHeaderValidationTests+XCTest.swift+43 −0 added@@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// HTTPHeaderValidationTests+XCTest.swift +// +import XCTest + +/// +/// NOTE: This file was generated by generate_linux_tests.rb +/// +/// Do NOT edit this file directly as it will be regenerated automatically when needed. +/// + +extension HTTPHeaderValidationTests { + + @available(*, deprecated, message: "not actually deprecated. Just deprecated to allow deprecated tests (which test deprecated functionality) without warnings") + static var allTests : [(String, (HTTPHeaderValidationTests) -> () throws -> Void)] { + return [ + ("testEncodingInvalidHeaderFieldNamesInRequests", testEncodingInvalidHeaderFieldNamesInRequests), + ("testEncodingInvalidTrailerFieldNamesInRequests", testEncodingInvalidTrailerFieldNamesInRequests), + ("testEncodingInvalidHeaderFieldValuesInRequests", testEncodingInvalidHeaderFieldValuesInRequests), + ("testEncodingInvalidTrailerFieldValuesInRequests", testEncodingInvalidTrailerFieldValuesInRequests), + ("testEncodingInvalidHeaderFieldNamesInResponses", testEncodingInvalidHeaderFieldNamesInResponses), + ("testEncodingInvalidTrailerFieldNamesInResponses", testEncodingInvalidTrailerFieldNamesInResponses), + ("testEncodingInvalidHeaderFieldValuesInResponses", testEncodingInvalidHeaderFieldValuesInResponses), + ("testEncodingInvalidTrailerFieldValuesInResponses", testEncodingInvalidTrailerFieldValuesInResponses), + ("testDisablingValidationClientSide", testDisablingValidationClientSide), + ("testDisablingValidationServerSide", testDisablingValidationServerSide), + ] + } +} +
Tests/NIOHTTP1Tests/HTTPRequestEncoderTest+XCTest.swift+1 −1 modified@@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information
Tests/NIOHTTP1Tests/HTTPResponseEncoderTest+XCTest.swift+1 −1 modified@@ -2,7 +2,7 @@ // // This source file is part of the SwiftNIO open source project // -// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors +// Copyright (c) 2017-2022 Apple Inc. and the SwiftNIO project authors // Licensed under Apache License v2.0 // // See LICENSE.txt for license information
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.