CVE-2022-31005 (published 2022-05-31T20:15Z)

Vapor is an HTTP web framework for Swift. Users of Vapor prior to version 4.60.3 with FileMiddleware enabled are vulnerable to an integer overflow vulnerability that can crash the application. Version 4.60.3 contains a patch for this issue. As a workaround, disable FileMiddleware and serve via a Content Delivery Network.

Severity: MEDIUM
CVSS v2 base score: 5.0 (AV:N/AC:L/Au:N/C:N/I:N/A:P)
CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-190
cwe_name: Integer Overflow or Wraparound
description: The product performs a calculation that can
         produce an integer overflow or wraparound when the logic
         assumes that the resulting value will always be larger than
         the original value. This occurs when an integer value is
         incremented to a value that is too large to store in the
         associated representation. When this occurs, the value may
         become a very small or negative number.
extended_description: 
            
            
            
            
         
         

Fix information:
repo_url: https://github.com/vapor/vapor
commit_details: repo_url: https://github.com/vapor/vapor
author: Tim Condon
author_date: 2022-05-31 10:33:26+01:00
msg: Merge pull request from GHSA-vj2m-9f5j-mpr5

* Add failing test for crash

* Bounds check the range and add a couple more test cases

* Fail if range start is negative

* Safer parsing of integer additions

* Check requested size for range header

* Check for 0

* Log errors and catch invalid range headers

* One more log
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: FileIO.swift
change_type: ModificationType.MODIFY
diff: @@ -1,4 +1,5 @@
 import NIO
+import Logging
 
 extension Request {
     public var fileio: FileIO {
@@ -138,6 +139,11 @@ public struct FileIO {
             } else {
                 contentRange = nil
             }
+        } else if request.headers.contains(name: .range) {
+            // Range header was supplied but could not be parsed i.e. it was invalid
+            request.logger.debug("Range header was provided in request but was invalid")
+            let response = Response(status: .badRequest)
+            return response
         } else {
             contentRange = nil
         }
@@ -163,7 +169,12 @@ public struct FileIO {
             if let firstRange = contentRange.ranges.first {
                 let range = firstRange.asResponseContentRange(limit: fileSize)
                 response.headers.contentRange = HTTPHeaders.ContentRange(unit: contentRange.unit, range: range)
-                (offset, byteCount) = firstRange.asByteBufferBounds(withMaxSize: fileSize)
+                do {
+                    (offset, byteCount) = try firstRange.asByteBufferBounds(withMaxSize: fileSize, logger: request.logger)
+                } catch {
+                    let response = Response(status: .badRequest)
+                    return response
+                }
             } else {
                 offset = 0
                 byteCount = fileSize
@@ -252,14 +263,31 @@ public struct FileIO {
 
 extension HTTPHeaders.Range.Value {
     
-    fileprivate func asByteBufferBounds(withMaxSize size: Int) -> (offset: Int64, byteCount: Int) {
+    fileprivate func asByteBufferBounds(withMaxSize size: Int, logger: Logger) throws -> (offset: Int64, byteCount: Int) {
         switch self {
             case .start(let value):
+                guard value <= size, value >= 0 else {
+                    logger.debug("Requested range start was invalid: \(value)")
+                    throw Abort(.badRequest)
+                }
                 return (offset: numericCast(value), byteCount: size - value)
             case .tail(let value):
+                guard value <= size, value >= 0 else {
+                    logger.debug("Requested range end was invalid: \(value)")
+                    throw Abort(.badRequest)
+                }
                 return (offset: numericCast(size - value), byteCount: value)
             case .within(let start, let end):
-                return (offset: numericCast(start), byteCount: end - start + 1)
+                guard start >= 0, end >= 0, start < end else {
+                    logger.debug("Requested range was invalid: \(start)-\(end)")
+                    throw Abort(.badRequest)
+                }
+                let (byteCount, overflow) =  (end - start).addingReportingOverflow(1)
+                guard !overflow else {
+                    logger.debug("Requested range was invalid: \(start)-\(end)")
+                    throw Abort(.badRequest)
+                }
+                return (offset: numericCast(start), byteCount: byteCount)
         }
     }
 }

programming_language: Swift
filename: FileTests.swift
change_type: ModificationType.MODIFY
diff: @@ -246,4 +246,55 @@ final class FileTests: XCTestCase {
             XCTAssertEqual(res.status, .notFound)
         }
     }
+    
+    // https://github.com/vapor/vapor/security/advisories/GHSA-vj2m-9f5j-mpr5
+    func testInvalidRangeHeaderDoesNotCrash() throws {
+        let app = Application(.testing)
+        defer { app.shutdown() }
+
+        app.get("file-stream") { req in
+            return req.fileio.streamFile(at: #file)
+        }
+
+        var headers = HTTPHeaders()
+        headers.replaceOrAdd(name: .range, value: "bytes=0-9223372036854775807")
+        try app.testable(method: .running).test(.GET, "/file-stream", headers: headers) { res in
+            XCTAssertEqual(res.status, .badRequest)
+        }
+        
+        headers.replaceOrAdd(name: .range, value: "bytes=-1-10")
+        try app.testable(method: .running).test(.GET, "/file-stream", headers: headers) { res in
+            XCTAssertEqual(res.status, .badRequest)
+        }
+        
+        headers.replaceOrAdd(name: .range, value: "bytes=100-10")
+        try app.testable(method: .running).test(.GET, "/file-stream", headers: headers) { res in
+            XCTAssertEqual(res.status, .badRequest)
+        }
+        
+        headers.replaceOrAdd(name: .range, value: "bytes=10--100")
+        try app.testable(method: .running).test(.GET, "/file-stream", headers: headers) { res in
+            XCTAssertEqual(res.status, .badRequest)
+        }
+        
+        headers.replaceOrAdd(name: .range, value: "bytes=9223372036854775808-")
+        try app.testable(method: .running).test(.GET, "/file-stream", headers: headers) { res in
+            XCTAssertEqual(res.status, .badRequest)
+        }
+        
+        headers.replaceOrAdd(name: .range, value: "bytes=922337203-")
+        try app.testable(method: .running).test(.GET, "/file-stream", headers: headers) { res in
+            XCTAssertEqual(res.status, .badRequest)
+        }
+        
+        headers.replaceOrAdd(name: .range, value: "bytes=-922337203")
+        try app.testable(method: .running).test(.GET, "/file-stream", headers: headers) { res in
+            XCTAssertEqual(res.status, .badRequest)
+        }
+        
+        headers.replaceOrAdd(name: .range, value: "bytes=-9223372036854775808")
+        try app.testable(method: .running).test(.GET, "/file-stream", headers: headers) { res in
+            XCTAssertEqual(res.status, .badRequest)
+        }
+    }
 }

programming_language: Swift