Integer Overflow in Vapor's HTTP Range Request
Description
Vapor FileMiddleware with integer overflow in HTTP range parsing leads to denial of service; fixed in 4.60.3.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Vapor FileMiddleware with integer overflow in HTTP range parsing leads to denial of service; fixed in 4.60.3.
Vulnerability
Vapor versions prior to 4.60.3 with FileMiddleware enabled contain an integer overflow vulnerability when processing HTTP Range headers [1][4]. Crafted range values such as bytes=0-9223372036854775807 cause an integer overflow, crashing the application [2][4]. This affects all Vapor apps using FileMiddleware without the patch.
Exploitation
An attacker only needs network access to send a single HTTP request with a malicious Range header [4]. The request can target any file served by FileMiddleware. No authentication or user interaction is required. The server then crashes as the range parsing overflow leads to undefined behavior [2].
Impact
Successful exploitation causes a denial of service (DoS) by crashing the application process [1][4]. The application becomes unavailable until restarted. No data integrity or confidentiality is compromised; the impact is limited to availability.
Mitigation
Vapor version 4.60.3 patches the vulnerability by properly validating range headers and returning HTTP 400 for invalid ranges [2][3]. Users should upgrade immediately. As a workaround, disable FileMiddleware and serve static files via a Content Delivery Network [1][4].
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/vapor/vaporSwiftURL | < 4.60.3 | 4.60.3 |
Affected products
2Patches
1953a349b539bMerge pull request from GHSA-vj2m-9f5j-mpr5
2 files changed · +82 −3
Sources/Vapor/Utilities/FileIO.swift+31 −3 modified@@ -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) } } }
Tests/VaporTests/FileTests.swift+51 −0 modified@@ -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) + } + } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-vj2m-9f5j-mpr5ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-31005ghsaADVISORY
- github.com/vapor/vapor/commit/953a349b539b3e0d3653585c8ffb50c427986df1ghsax_refsource_MISCWEB
- github.com/vapor/vapor/releases/tag/4.60.3ghsax_refsource_MISCWEB
- github.com/vapor/vapor/security/advisories/GHSA-vj2m-9f5j-mpr5ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.