CVE-2023-44386 (published 2023-10-05T18:15Z)

Vapor is an HTTP web framework for Swift. There is a denial of service vulnerability impacting all users of affected versions of Vapor. The HTTP1 error handler closed connections when HTTP parse errors occur instead of passing them on. The issue is fixed as of Vapor release 4.84.2.

CVSS v3 base score: 5.3, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L)

Weakness information:
cwe_id: CWE-231
cwe_name: Improper Handling of Extra Values
description: The product does not handle or incorrectly handles when more values are provided than expected.
extended_description: 
            
            
         
         
cwe_id: CWE-617
cwe_name: Reachable Assertion
description: The product contains an assert() or similar statement that can be triggered by an attacker, which leads to an application exit or other behavior that is more severe than necessary.
extended_description: 
            While assertion is good for catching logic errors and reducing the chances of reaching more serious vulnerability conditions, it can still lead to a denial of service.
            For example, if a server handles multiple simultaneous connections, and an assert() occurs in one single connection that causes all other connections to be dropped, this is a reachable assertion that leads to a denial of service.
         
         
cwe_id: CWE-696
cwe_name: Incorrect Behavior Order
description: The product performs multiple related behaviors, but the behaviors are performed in the wrong order in ways which may produce resultant weaknesses.
extended_description: 
            
         
         

Fix information:
repo_url: https://github.com/vapor/vapor
commit_details: repo_url: https://github.com/vapor/vapor
author: Gwynne Raskind
author_date: 2023-10-05 06:20:16-05:00
msg: Merge pull request from GHSA-3mwq-h3g6-ffhm

* Fix the HTTP1 error handler to close connections when HTTP parse errors occur instead of passing them on.

* Move the error handling for parser errors into HTTPServerRequestDecoder, where it arguably belongs. Don't send 400 bad request for those errors (not required by RFC, difficult to do safely for all cases), fix inbound user event handling in multiple handlers.
repository: repo_url: https://github.com/vapor/vapor
repo_name: vapor/vapor
description: 💧 A server-side Swift HTTP web framework.
repo_language: Swift
owner: vapor
file_changes: filename: HTTPServer.swift
change_type: ModificationType.MODIFY
diff: @@ -452,73 +452,6 @@ private final class HTTPServerConnection: Sendable {
     }
 }
 
-/// A simple channel handler that catches errors emitted by parsing HTTP requests
-/// and sends 400 Bad Request responses.
-///
-/// This channel handler provides the basic behaviour that the majority of simple HTTP
-/// servers want. This handler does not suppress the parser errors: it allows them to
-/// continue to pass through the pipeline so that other handlers (e.g. logging ones) can
-/// deal with the error.
-/// 
-/// adapted from: https://github.com/apple/swift-nio/blob/00341c92770e0a7bebdc5fda783f08765eb3ff56/Sources/NIOHTTP1/HTTPServerProtocolErrorHandler.swift
-final class HTTP1ServerErrorHandler: ChannelDuplexHandler, RemovableChannelHandler {
-    typealias InboundIn = Never
-    typealias InboundOut = Never
-    typealias OutboundIn = HTTPServerResponsePart
-    typealias OutboundOut = HTTPServerResponsePart
-    let logger: Logger
-    private var hasUnterminatedResponse: Bool = false
-    
-    init(logger: Logger) {
-        self.logger = logger
-    }
-    
-    func errorCaught(context: ChannelHandlerContext, error: Error) {
-        if let error = error as? HTTPParserError {
-            self.makeHTTPParserErrorResponse(context: context, error: error)
-        }
-
-        // Now pass the error on in case someone else wants to see it.
-        // In the Vapor ChannelPipeline the connection will eventually 
-        // be closed by the NIOCloseOnErrorHandler
-        context.fireErrorCaught(error)
-    }
-
-    private func makeHTTPParserErrorResponse(context: ChannelHandlerContext, error: HTTPParserError) {
-        // Any HTTPParserError is automatically fatal, and we don't actually need (or want) to
-        // provide that error to the client: we just want to inform them something went wrong
-        // and then close off the pipeline. However, we can only send an
-        // HTTP error response if another response hasn't started yet.
-        //
-        // A side note here: we cannot block or do any delayed work. 
-        // The channel might be closed right after we return from this function.
-        if !self.hasUnterminatedResponse {
-            self.logger.debug("Bad Request - Invalid HTTP: \(error)")
-            let headers = HTTPHeaders([("Connection", "close"), ("Content-Length", "0")])
-            let head = HTTPResponseHead(version: .http1_1, status: .badRequest, headers: headers)
-            context.write(self.wrapOutboundOut(.head(head)), promise: nil)
-            context.writeAndFlush(self.wrapOutboundOut(.end(nil)), promise: nil)
-        }
-    }
-
-    public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
-        let res = self.unwrapOutboundIn(data)
-        switch res {
-        case .head(let head) where head.isInformational:
-            precondition(!self.hasUnterminatedResponse)
-        case .head:
-            precondition(!self.hasUnterminatedResponse)
-            self.hasUnterminatedResponse = true
-        case .body:
-            precondition(self.hasUnterminatedResponse)
-        case .end:
-            precondition(self.hasUnterminatedResponse)
-            self.hasUnterminatedResponse = false
-        }
-        context.write(data, promise: promise)
-    }
-}
-
 extension HTTPResponseHead {
     /// Determines if the head is purely informational. If a head is informational another head will follow this
     /// head eventually.
@@ -608,9 +541,6 @@ extension ChannelPipeline {
             break
         }
 
-        let errorHandler = HTTP1ServerErrorHandler(logger: configuration.logger)
-        handlers.append(errorHandler)
-
         // add NIO -> HTTP response encoder
         let serverResEncoder = HTTPServerResponseEncoder(
             serverHeader: configuration.serverName,

programming_language: Swift
filename: HTTPServerHandler.swift
change_type: ModificationType.MODIFY
diff: @@ -71,7 +71,7 @@ final class HTTPServerHandler: ChannelInboundHandler, RemovableChannelHandler {
             self.logger.trace("HTTP handler will no longer respect keep-alive")
             self.isShuttingDown = true
         default:
-            self.logger.trace("Unhandled user event: \(event)")
+            context.fireUserInboundEventTriggered(event)
         }
     }
 }

programming_language: Swift
filename: HTTPServerRequestDecoder.swift
change_type: ModificationType.MODIFY
diff: @@ -133,6 +133,10 @@ final class HTTPServerRequestDecoder: ChannelDuplexHandler, RemovableChannelHand
         default:
             break
         }
+
+        if error is HTTPParserError {
+            self.logger.debug("Invalid HTTP request, will close connection: \(String(reflecting: error))")
+        }
         context.fireErrorCaught(error)
     }
 
@@ -218,7 +222,7 @@ final class HTTPServerRequestDecoder: ChannelDuplexHandler, RemovableChannelHand
                 context.fireUserInboundEventTriggered(event)
             }
         default:
-            self.logger.trace("Unhandled user event: \(event)")
+            context.fireUserInboundEventTriggered(event)
         }
     }
 }

programming_language: Swift
filename: PipelineTests.swift
change_type: ModificationType.MODIFY
diff: @@ -138,7 +138,6 @@ final class PipelineTests: XCTestCase {
             }
         }
         XCTAssertEqual(channel.isActive, false)
-        try XCTAssertContains(channel.readOutbound(as: ByteBuffer.self)?.string, "HTTP/1.1 400 Bad Request")
         try XCTAssertNil(channel.readOutbound(as: ByteBuffer.self)?.string)
     }
 

programming_language: Swift