CVE-2022-31005 (Swift, published 2022-05-31T20:15Z)
Description: value: 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 v3 base score: 7.5)
Weakness: CWE-190 \u2014 Integer Overflow or Wraparound
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.
Repository: https://github.com/vapor/vapor
Files changed:
Sources/Vapor/Utilities/FileIO.swift
Tests/VaporTests/FileTests.swift
Commit message: 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

Vulnerable code:
                (offset, byteCount) = firstRange.asByteBufferBounds(withMaxSize: fileSize)
    fileprivate func asByteBufferBounds(withMaxSize size: Int) -> (offset: Int64, byteCount: Int) {
                return (offset: numericCast(start), byteCount: end - start + 1)

Fixed code:
import Logging
        } 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
                do {
                    (offset, byteCount) = try firstRange.asByteBufferBounds(withMaxSize: fileSize, logger: request.logger)
                } catch {
                    let response = Response(status: .badRequest)
                    return response
                }
    fileprivate func asByteBufferBounds(withMaxSize size: Int, logger: Logger) throws -> (offset: Int64, byteCount: Int) {
                guard value <= size, value >= 0 else {
                    logger.debug("Requested range start was invalid: \(value)")
                    throw Abort(.badRequest)
                }
                guard value <= size, value >= 0 else {
                    logger.debug("Requested range end was invalid: \(value)")
                    throw Abort(.badRequest)
                }
                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)
    
    // 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)
        }
    }