VYPR
Medium severity6.3GHSA Advisory· Published Jun 12, 2026· Updated Jun 12, 2026

SwiftNIO: CRLF Injection in outbound HTTP request URI via NIOHTTPRequestHeadersValidator

CVE-2026-28970

Description

CRLF injection in swift-nio HTTP/1.1 start line allows request smuggling and response splitting in versions 2.0.0–2.99.0; fixed in 2.100.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

CRLF injection in swift-nio HTTP/1.1 start line allows request smuggling and response splitting in versions 2.0.0–2.99.0; fixed in 2.100.0.

Vulnerability

The vulnerability is a CRLF injection in the HTTP/1.1 start line components (request URI, request method, and response reason phrase) due to insufficient validation by NIOHTTPRequestHeadersValidator and NIOHTTPResponseHeadersValidator channel handlers. All versions of swift-nio from 2.0.0 to 2.99.0 are affected [1][2].

Exploitation

An attacker must be able to influence the content of outbound HTTP start line fields. In proxy applications, attacker-controlled URIs or methods are directly forwarded. For clients, a malicious server can trigger a redirect to a crafted URL. For servers, a client can cause a crafted response reason phrase. Injecting a CRLF sequence requires a single crafted request [1][2].

Impact

Successful exploitation enables HTTP request smuggling or HTTP response splitting, potentially bypassing WAFs, poisoning web caches, or smuggling requests past intermediaries [1][2].

Mitigation

Fixed in swift-nio version 2.100.0 and later. As a workaround, ensure all user-controlled input is sanitized before inclusion in HTTP start line components [1][2].

AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

1

Patches

1
dd16365724d5

Merge commit from fork

https://github.com/apple/swift-nioFabian FettMay 20, 2026Fixed in 2.100.0via ghsa-release-walk
2 files changed · +511 4
  • Sources/NIOHTTP1/HTTPHeaderValidator.swift+243 4 modified
    @@ -20,7 +20,7 @@ import NIOCore
     /// 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,
    +/// ``NIOHTTPRequestHeadersValidator`` will also validate that the HTTP trailers are within specification,
     /// if they are present.
     public final class NIOHTTPRequestHeadersValidator: ChannelOutboundHandler, RemovableChannelHandler {
         public typealias OutboundIn = HTTPClientRequestPart
    @@ -31,7 +31,7 @@ public final class NIOHTTPRequestHeadersValidator: ChannelOutboundHandler, Remov
         public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
             switch NIOHTTPRequestHeadersValidator.unwrapOutboundIn(data) {
             case .head(let head):
    -            guard head.headers.areValidToSend else {
    +            guard Self.uriOnlyContainsAllowedCharacters(head.uri), head.method.isValidToSend, head.headers.areValidToSend else {
                     promise?.fail(HTTPParserError.invalidHeaderToken)
                     context.fireErrorCaught(HTTPParserError.invalidHeaderToken)
                     return
    @@ -48,6 +48,79 @@ public final class NIOHTTPRequestHeadersValidator: ChannelOutboundHandler, Remov
     
             context.write(data, promise: promise)
         }
    +
    +    static func uriOnlyContainsAllowedCharacters(_ uri: String) -> Bool {
    +        // The spec in [RFC 9112](https://datatracker.ietf.org/doc/html/rfc9112#section-3.2) defines the valid
    +        // characters for the request-target as the following:
    +        //
    +        // ```
    +        // request-target = origin-form / absolute-form / authority-form / asterisk-form
    +        //
    +        // origin-form    = absolute-path [ "?" query ]
    +        // absolute-form  = absolute-URI
    +        // authority-form = uri-host ":" port              ; CONNECT only
    +        // asterisk-form  = "*"                            ; OPTIONS only
    +        // ```
    +        //
    +        // The component grammar comes from [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-3)
    +        // (updated by [RFC 8820](https://datatracker.ietf.org/doc/html/rfc8820), which adds best-practice
    +        // guidance for URI design but does not change the syntax):
    +        //
    +        // ```
    +        // absolute-path = 1*( "/" segment )
    +        // segment       = *pchar
    +        // query         = *( pchar / "/" / "?" )
    +        //
    +        // pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
    +        // pct-encoded   = "%" HEXDIG HEXDIG
    +        //
    +        // unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
    +        // reserved      = gen-delims / sub-delims
    +        // gen-delims    = ":" / "/" / "?" / "#" / "[" / "]" / "@"
    +        // sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
    +        //               / "*" / "+" / "," / ";" / "="
    +        // ```
    +        //
    +        // In other words, the literal byte set allowed on the wire is:
    +        //
    +        // ```
    +        //   ALPHA          %x41-5A / %x61-7A      ; A–Z a–z
    +        //   DIGIT          %x30-39                ; 0–9
    +        //   unreserved     "-" "." "_" "~"
    +        //   gen-delims     ":" "/" "?" "#" "[" "]" "@"
    +        //   sub-delims     "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "="
    +        //   pct-encoded    "%" HEXDIG HEXDIG      ; escape for anything else
    +        // ```
    +        //
    +        // Everything outside this set — SP, CTLs (%x00-1F / %x7F), non-ASCII (%x80-FF),
    +        // and `" < > \ ^ ` { | }` — MUST be percent-encoded. Bare CR, LF, or NUL in
    +        // the request-target MUST be rejected (request smuggling / response splitting).
    +
    +        uri.utf8.allSatisfy { byte in
    +            switch byte {
    +            case
    +                // unreserved
    +                //   - ALPHA
    +                UInt8(ascii: "A")...UInt8(ascii: "Z"), UInt8(ascii: "a")...UInt8(ascii: "z"),
    +                //   - DIGIT
    +                UInt8(ascii: "0")...UInt8(ascii: "9"),
    +                //   - extra characters
    +                UInt8(ascii: "-"), UInt8(ascii: "."), UInt8(ascii: "_"), UInt8(ascii: "~"),
    +                // gen-delims
    +                UInt8(ascii: ":"), UInt8(ascii: "/"), UInt8(ascii: "?"), UInt8(ascii: "#"),
    +                UInt8(ascii: "["), UInt8(ascii: "]"), UInt8(ascii: "@"),
    +                // sub-delims
    +                UInt8(ascii: "!"), UInt8(ascii: "$"), UInt8(ascii: "&"), UInt8(ascii: "'"),
    +                UInt8(ascii: "("), UInt8(ascii: ")"), UInt8(ascii: "*"), UInt8(ascii: "+"),
    +                UInt8(ascii: ","), UInt8(ascii: ";"), UInt8(ascii: "="),
    +                // pct-encoded
    +                UInt8(ascii: "%"):
    +                return true
    +            default:
    +                return false
    +            }
    +        }
    +    }
     }
     
     /// A ChannelHandler to validate that outbound response headers are spec-compliant.
    @@ -57,7 +130,7 @@ public final class NIOHTTPRequestHeadersValidator: ChannelOutboundHandler, Remov
     /// 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,
    +/// ``NIOHTTPResponseHeadersValidator`` will also validate that the HTTP trailers are within specification,
     /// if they are present.
     public final class NIOHTTPResponseHeadersValidator: ChannelOutboundHandler, RemovableChannelHandler {
         public typealias OutboundIn = HTTPServerResponsePart
    @@ -79,7 +152,7 @@ public final class NIOHTTPResponseHeadersValidator: ChannelOutboundHandler, Remo
         public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
             switch (NIOHTTPResponseHeadersValidator.unwrapOutboundIn(data), self.state) {
             case (.head(let head), .validating):
    -            if head.headers.areValidToSend {
    +            if head.headers.areValidToSend, head.status.isValidToSend {
                     context.write(data, promise: promise)
                 } else {
                     self.state = .dropping
    @@ -111,3 +184,169 @@ extension NIOHTTPRequestHeadersValidator: Sendable {}
     
     @available(*, unavailable)
     extension NIOHTTPResponseHeadersValidator: Sendable {}
    +
    +extension HTTPMethod {
    +    /// Whether these HTTPHeaders are valid to send on the wire.
    +    var isValidToSend: Bool {
    +        switch self {
    +        case .GET,
    +            .PUT,
    +            .ACL,
    +            .HEAD,
    +            .POST,
    +            .COPY,
    +            .LOCK,
    +            .MOVE,
    +            .BIND,
    +            .LINK,
    +            .PATCH,
    +            .TRACE,
    +            .MKCOL,
    +            .MERGE,
    +            .PURGE,
    +            .NOTIFY,
    +            .SEARCH,
    +            .UNLOCK,
    +            .REBIND,
    +            .UNBIND,
    +            .REPORT,
    +            .DELETE,
    +            .UNLINK,
    +            .CONNECT,
    +            .MSEARCH,
    +            .OPTIONS,
    +            .PROPFIND,
    +            .CHECKOUT,
    +            .PROPPATCH,
    +            .SUBSCRIBE,
    +            .MKCALENDAR,
    +            .MKACTIVITY,
    +            .UNSUBSCRIBE,
    +            .SOURCE:
    +            true
    +
    +        case .RAW(value: let value):
    +            // The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#method.overview) defines the valid
    +            // characters as the following:
    +            //
    +            // ```
    +            // method = token
    +            //
    +            // token          = 1*tchar
    +            //
    +            // tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
    +            //                / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
    +            //                / DIGIT / ALPHA
    +            //                ; any VCHAR, except delimiters
    +
    +            value.utf8.allSatisfy { byte in
    +                switch byte {
    +                case
    +                    // ALPHA
    +                    UInt8(ascii: "A")...UInt8(ascii: "Z"), UInt8(ascii: "a")...UInt8(ascii: "z"),
    +                    // DIGIT
    +                    UInt8(ascii: "0")...UInt8(ascii: "9"),
    +                    // token
    +                    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: "~"):
    +                    true
    +                default:
    +                    false
    +                }
    +            }
    +        }
    +    }
    +}
    +
    +extension HTTPResponseStatus {
    +    var isValidToSend: Bool {
    +        switch self {
    +        case .continue,
    +            .switchingProtocols,
    +            .processing,
    +            .ok,
    +            .created,
    +            .accepted,
    +            .nonAuthoritativeInformation,
    +            .noContent,
    +            .resetContent,
    +            .partialContent,
    +            .multiStatus,
    +            .alreadyReported,
    +            .imUsed,
    +            .multipleChoices,
    +            .movedPermanently,
    +            .found,
    +            .seeOther,
    +            .notModified,
    +            .useProxy,
    +            .temporaryRedirect,
    +            .permanentRedirect,
    +            .badRequest,
    +            .unauthorized,
    +            .paymentRequired,
    +            .forbidden,
    +            .notFound,
    +            .methodNotAllowed,
    +            .notAcceptable,
    +            .proxyAuthenticationRequired,
    +            .requestTimeout,
    +            .conflict,
    +            .gone,
    +            .lengthRequired,
    +            .preconditionFailed,
    +            .payloadTooLarge,
    +            .uriTooLong,
    +            .unsupportedMediaType,
    +            .rangeNotSatisfiable,
    +            .expectationFailed,
    +            .imATeapot,
    +            .misdirectedRequest,
    +            .unprocessableEntity,
    +            .locked,
    +            .failedDependency,
    +            .upgradeRequired,
    +            .preconditionRequired,
    +            .tooManyRequests,
    +            .requestHeaderFieldsTooLarge,
    +            .unavailableForLegalReasons,
    +            .internalServerError,
    +            .notImplemented,
    +            .badGateway,
    +            .serviceUnavailable,
    +            .gatewayTimeout,
    +            .httpVersionNotSupported,
    +            .variantAlsoNegotiates,
    +            .insufficientStorage,
    +            .loopDetected,
    +            .notExtended,
    +            .networkAuthenticationRequired:
    +            true
    +
    +        case .custom(_, let reasonPhrase):
    +            // The spec in [RFC 9112](https://datatracker.ietf.org/doc/html/rfc9112#section-4) defines the valid
    +            // characters as the following:
    +            //
    +            // ```
    +            // reason-phrase = 1*( HTAB / SP / VCHAR / obs-text )
    +            //
    +            // obs-text      = %x80-FF
    +            // ```
    +
    +            reasonPhrase.utf8.allSatisfy { byte in
    +                switch byte {
    +                case
    +                    9,         // HTAB
    +                    32,        // SP
    +                    33...126,  // VCHAR
    +                    128...255: // obs-text
    +                    return true
    +                default:
    +                    return false
    +                }
    +            }
    +        }
    +    }
    +}
    
  • Tests/NIOHTTP1Tests/HTTPHeaderValidationTests.swift+268 0 modified
    @@ -501,6 +501,223 @@ import Testing
             }
         }
     
    +    @Test func encodingInvalidUriInRequest() throws {
    +        // The spec in [RFC 9112](https://datatracker.ietf.org/doc/html/rfc9112#section-3.2) defines the valid
    +        // characters for the request-target as the following:
    +        //
    +        // ```
    +        // request-target = origin-form / absolute-form / authority-form / asterisk-form
    +        //
    +        // origin-form    = absolute-path [ "?" query ]
    +        // absolute-form  = absolute-URI
    +        // authority-form = uri-host ":" port              ; CONNECT only
    +        // asterisk-form  = "*"                            ; OPTIONS only
    +        // ```
    +        //
    +        // The component grammar comes from [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-3)
    +        // (updated by [RFC 8820](https://datatracker.ietf.org/doc/html/rfc8820), which adds best-practice
    +        // guidance for URI design but does not change the syntax):
    +        //
    +        // ```
    +        // absolute-path = 1*( "/" segment )
    +        // segment       = *pchar
    +        // query         = *( pchar / "/" / "?" )
    +        //
    +        // pchar         = unreserved / pct-encoded / sub-delims / ":" / "@"
    +        // pct-encoded   = "%" HEXDIG HEXDIG
    +        //
    +        // unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"
    +        // reserved      = gen-delims / sub-delims
    +        // gen-delims    = ":" / "/" / "?" / "#" / "[" / "]" / "@"
    +        // sub-delims    = "!" / "$" / "&" / "'" / "(" / ")"
    +        //               / "*" / "+" / "," / ";" / "="
    +        // ```
    +        //
    +        // In other words, the literal byte set allowed on the wire is:
    +        //
    +        // ```
    +        //   ALPHA          %x41-5A / %x61-7A      ; A–Z a–z
    +        //   DIGIT          %x30-39                ; 0–9
    +        //   unreserved     "-" "." "_" "~"
    +        //   gen-delims     ":" "/" "?" "#" "[" "]" "@"
    +        //   sub-delims     "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "="
    +        //   pct-encoded    "%" HEXDIG HEXDIG      ; escape for anything else
    +        // ```
    +        //
    +        // Everything outside this set — SP, CTLs (%x00-1F / %x7F), non-ASCII (%x80-FF),
    +        // and `" < > \ ^ ` { | }` — MUST be percent-encoded. Bare CR, LF, or NUL in
    +        // the request-target MUST be rejected (request smuggling / response splitting).
    +
    +        let allowed = "-._~:/?#[]@!$&'()*+,;=%0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    +
    +        let channel = EmbeddedChannel()
    +        try channel.pipeline.syncOperations.addHTTPClientHandlers()
    +
    +        let headers = HTTPHeaders([("Host", "example.com")])
    +        let goodRequest = HTTPRequestHead(version: .http1_1, method: .GET, uri: allowed, headers: headers)
    +        let goodRequestBytes = ByteBuffer(
    +            string: "GET \(allowed) HTTP/1.1\r\nHost: example.com\r\n\r\n"
    +        )
    +
    +        #expect(throws: Never.self) { try channel.writeOutbound(HTTPClientRequestPart.head(goodRequest)) }
    +        #expect(throws: Never.self) { try channel.writeOutbound(HTTPClientRequestPart.end(nil)) }
    +        #expect(try channel.readOutbound(as: ByteBuffer.self) == goodRequestBytes)
    +
    +        // Now confirm all other bytes are rejected.
    +        for byte in UInt8(0)...UInt8(255) {
    +            // Skip bytes that we already believe are allowed.
    +            if allowed.utf8.contains(byte) {
    +                continue
    +            }
    +
    +            guard let disallowedBytes = Self.makeStringContainingLiteralByte(byte) else {
    +                continue
    +            }
    +
    +            let forbiddenUri = allowed + disallowedBytes
    +
    +            let channel = EmbeddedChannel()
    +            try channel.pipeline.syncOperations.addHTTPClientHandlers()
    +
    +            let headers = HTTPHeaders([("Host", "example.com")])
    +            let badRequest = HTTPRequestHead(version: .http1_1, method: .GET, uri: forbiddenUri, headers: headers)
    +
    +            let error = #expect(
    +                throws: HTTPParserError.self,
    +                "Incorrectly tolerated character in method: \(String(decoding: [byte], as: UTF8.self))"
    +            ) {
    +                try channel.writeOutbound(HTTPClientRequestPart.head(badRequest))
    +            }
    +            #expect(error == .invalidHeaderToken)
    +            _ = try? channel.finish()
    +        }
    +    }
    +
    +    @Test func encodingInvalidMethodInRequests() throws {
    +        // The spec in [RFC 9110](https://httpwg.org/specs/rfc9110.html#method.overview) defines the valid
    +        // characters as the following:
    +        //
    +        // ```
    +        // method = token
    +        //
    +        // token          = 1*tchar
    +        //
    +        // tchar          = "!" / "#" / "$" / "%" / "&" / "'" / "*"
    +        //                / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
    +        //                / DIGIT / ALPHA
    +        //                ; any VCHAR, except delimiters
    +        let weirdAllowedMethodName =
    +            "!#$%&'*+-.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    +
    +        let channel = EmbeddedChannel()
    +        try channel.pipeline.syncOperations.addHTTPClientHandlers()
    +
    +        let headers = HTTPHeaders([("Host", "example.com")])
    +        let goodRequest = HTTPRequestHead(version: .http1_1, method: .RAW(value: weirdAllowedMethodName), uri: "/", headers: headers)
    +        let goodRequestBytes = ByteBuffer(
    +            string: "\(weirdAllowedMethodName) / HTTP/1.1\r\nHost: example.com\r\n\r\n"
    +        )
    +
    +        #expect(throws: Never.self) { try channel.writeOutbound(HTTPClientRequestPart.head(goodRequest)) }
    +        #expect(throws: Never.self) { try channel.writeOutbound(HTTPClientRequestPart.end(nil)) }
    +        #expect(try channel.readOutbound(as: ByteBuffer.self) == goodRequestBytes)
    +
    +        // Now confirm all other bytes are rejected.
    +        for byte in UInt8(0)...UInt8(255) {
    +            // Skip bytes that we already believe are allowed.
    +            if weirdAllowedMethodName.utf8.contains(byte) {
    +                continue
    +            }
    +            guard let extraString = Self.makeStringContainingLiteralByte(byte) else {
    +                continue
    +            }
    +
    +            let forbiddenFieldName = weirdAllowedMethodName + extraString
    +
    +            let channel = EmbeddedChannel()
    +            try channel.pipeline.syncOperations.addHTTPClientHandlers()
    +
    +            let headers = HTTPHeaders([("Host", "example.com")])
    +            let badRequest = HTTPRequestHead(version: .http1_1, method: .RAW(value: forbiddenFieldName), uri: "/", headers: headers)
    +
    +            let error = #expect(
    +                throws: HTTPParserError.self,
    +                "Incorrectly tolerated character in method: \(extraString)"
    +            ) {
    +                try channel.writeOutbound(HTTPClientRequestPart.head(badRequest))
    +            }
    +            #expect(error == .invalidHeaderToken)
    +            _ = try? channel.finish()
    +        }
    +    }
    +
    +    @Test func encodingInvalidStatusReasonInResponses() throws {
    +        // The spec in [RFC 9112](https://datatracker.ietf.org/doc/html/rfc9112#section-4) defines the valid
    +        // characters as the following:
    +        //
    +        // ```
    +        // reason-phrase = 1*( HTAB / SP / VCHAR / obs-text )
    +        //
    +        // obs-text      = %x80-FF
    +        // ```
    +
    +        let allowedRanges: [ClosedRange<UInt8>] = [
    +            9...9,      // HTAB
    +            32...32,    // SP
    +            33...126,   // VCHAR
    +            128...255,  // obs-text
    +        ]
    +
    +        let base = "foo"
    +        let headers = HTTPHeaders([("transfer-encoding", "chunked")])
    +
    +        // Now confirm all other bytes in the ASCII range are rejected.
    +        for byte in UInt8(0)..<UInt8(255) {
    +            let allowed = allowedRanges.contains(where: { $0.contains(byte) })
    +
    +            guard let literalByteCanBeRepresented = Self.makeStringContainingLiteralByte(byte) else {
    +                continue
    +            }
    +
    +            let testReason = base + literalByteCanBeRepresented
    +
    +            let channel = EmbeddedChannel()
    +            try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: false)
    +            try channel.primeForResponse()
    +
    +            let response = HTTPResponseHead(
    +                version: .http1_1,
    +                status: .custom(code: 600, reasonPhrase: testReason),
    +                headers: headers
    +            )
    +
    +            switch allowed {
    +            case true:
    +                let goodRequestBytes = ByteBuffer(string: "HTTP/1.1 600 \(testReason)\r\ntransfer-encoding: chunked\r\n\r\n")
    +                let goodEnd = ByteBuffer(string: "0\r\n\r\n")
    +                #expect(throws: Never.self, "Rejected reason phrase with byte: \(byte)") {
    +                    try channel.writeOutbound(HTTPServerResponsePart.head(response))
    +                }
    +                let bytes = try channel.readOutbound(as: ByteBuffer.self)
    +                #expect(bytes == goodRequestBytes)
    +                #expect(throws: Never.self) {
    +                    try channel.writeOutbound(HTTPServerResponsePart.end(nil))
    +                }
    +                #expect(try channel.readOutbound(as: ByteBuffer.self) == goodEnd)
    +            case false:
    +                let error = #expect(
    +                    throws: HTTPParserError.self,
    +                    "Incorrectly tolerated character in reason phrase: \(String(decoding: [byte], as: UTF8.self))"
    +                ) {
    +                    try channel.writeOutbound(HTTPServerResponsePart.head(response))
    +                }
    +                #expect(error == .invalidHeaderToken)
    +            }
    +
    +            _ = try? channel.finish()
    +        }
    +    }
    +
         @Test func responseIsDroppedIfHeadersInvalid() throws {
             let channel = EmbeddedChannel()
             try channel.pipeline.syncOperations.configureHTTPServerPipeline(withErrorHandling: false)
    @@ -597,6 +814,56 @@ import Testing
             #expect(try channel.readOutbound(as: ByteBuffer.self) == toleratedResponseBytes)
             #expect(try channel.readOutbound(as: ByteBuffer.self) == toleratedTrailerBytes)
         }
    +
    +    /// Returns a valid UTF-8 string whose utf8 bytes *literally contain* `byte`.
    +    /// Returns nil if `byte` cannot appear in any valid UTF-8 sequence.
    +    static func makeStringContainingLiteralByte(_ byte: UInt8) -> String? {
    +        switch byte {
    +        case 0x00...0x7F:
    +            // ASCII — appears as itself
    +            return String(Unicode.Scalar(byte))
    +
    +        case 0x80...0xBF:
    +            // Continuation byte — pair with lead 0xC2 to form U+0080…U+00BF
    +            // UTF-8 of U+00XX (for 0x80…0xBF) is [0xC2, 0xXX]
    +            return String(Unicode.Scalar(byte))
    +
    +        case 0xC0, 0xC1:
    +            return nil   // forbidden in UTF-8
    +
    +        case 0xC2...0xDF:
    +            // Lead of 2-byte seq. Smallest scalar with this lead:
    +            // scalar = (byte & 0x1F) << 6  →  UTF-8 = [byte, 0x80]
    +            let scalar = UInt32(byte & 0x1F) << 6
    +            return Unicode.Scalar(scalar).map { String($0) }
    +
    +        case 0xE0...0xEF:
    +            // Lead of 3-byte seq. Smallest non-overlong scalar:
    +            //   0xE0 → U+0800 (special: must be ≥ 0x0800 to avoid overlong)
    +            //   0xE1…0xEF → (byte & 0x0F) << 12
    +            let base = UInt32(byte & 0x0F) << 12
    +            let scalar = (byte == 0xE0) ? 0x0800 : base
    +            // Skip surrogates if we land in D800–DFFF (byte == 0xED)
    +            if byte == 0xED { return String(Unicode.Scalar(0xD000 - 0x0800 + base)!) } // simple pick
    +            return Unicode.Scalar(scalar).map { String($0) }
    +
    +        case 0xF0...0xF4:
    +            // Lead of 4-byte seq.
    +            //   0xF0 → U+10000 (minimum to avoid overlong)
    +            //   0xF1…0xF3 → (byte & 0x07) << 18
    +            //   0xF4 → must be ≤ U+10FFFF
    +            let scalar: UInt32
    +            switch byte {
    +            case 0xF0: scalar = 0x10000
    +            case 0xF4: scalar = 0x100000
    +            default:   scalar = UInt32(byte & 0x07) << 18
    +            }
    +            return Unicode.Scalar(scalar).map { String($0) }
    +
    +        default: // 0xF5...0xFF
    +            return nil   // forbidden in UTF-8
    +        }
    +    }
     }
     
     extension EmbeddedChannel {
    @@ -605,3 +872,4 @@ extension EmbeddedChannel {
             try self.writeInbound(request)
         }
     }
    +
    

Vulnerability mechanics

Synthesis attempt was rejected by the grounding validator. Re-run pending.

References

2

News mentions

0

No linked articles in our index yet.