From c4a18f50feecb7bc392cde0160291cfb5a1464a5 Mon Sep 17 00:00:00 2001 From: Vladimir Kukushkin Date: Thu, 10 Sep 2026 13:37:46 +0100 Subject: [PATCH 1/2] Validate multipart boundary length against the RFC 2046 limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 2046 section 5.1.1 limits a `multipart/form-data` boundary to at most 70 characters. `requiredBoundary()` currently returns the boundary from the `Content-Type` header without checking its length, so a boundary of any size is accepted and handed straight to the parser. Besides being out of spec, an over-long boundary makes the parser's boundary matching do more work than it should for no legitimate benefit — a well-formed request never needs a boundary longer than the limit. - Reject a boundary whose UTF-8 length exceeds 70 in `requiredBoundary()`, throwing a new `RuntimeError.multipartBoundaryTooLong`. Matching is done on UTF-8 bytes, consistent with how the parser consumes the boundary. - Map `multipartBoundaryTooLong` to a `400 Bad Request` response, alongside the other malformed-`Content-Type` errors. - Add a unit test covering the boundaries at the limit (70, accepted) and just over it (71, rejected). Requests with a spec-compliant boundary decode exactly as before. A boundary longer than 70 bytes is now rejected up front with a clear error (surfaced as `400 Bad Request` on the server path) instead of being passed to the parser. --- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 5 ++++- .../Multipart/OpenAPIMIMEType+Multipart.swift | 2 ++ .../Base/Test_OpenAPIMIMEType.swift | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 6ea5caf5..d7619233 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -63,6 +63,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Multipart case missingRequiredMultipartFormDataContentType case missingMultipartBoundaryContentTypeParameter + case multipartBoundaryTooLong // Transport/Handler case transportFailed(any Error) @@ -118,6 +119,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret case .missingRequiredMultipartFormDataContentType: return "Expected a 'multipart/form-data' content type." case .missingMultipartBoundaryContentTypeParameter: return "Missing 'boundary' parameter in the 'multipart/form-data' content type." + case .multipartBoundaryTooLong: + return "Multipart boundary is too long; RFC 2046 allows at most 70 bytes." case .transportFailed: return "Transport threw an error." case .middlewareFailed(middlewareType: let type, _): return "Middleware of type '\(type)' threw an error." case .handlerFailed: return "User handler threw an error." @@ -166,7 +169,7 @@ extension RuntimeError: HTTPResponseConvertible { case .unexpectedAcceptHeader: .notAcceptable case .failedToDecodeStringConvertibleValue, .invalidAcceptSubstring, .invalidBase64String, .invalidHeaderFieldName, .malformedAcceptHeader, .missingMultipartBoundaryContentTypeParameter, - .missingOrMalformedContentDispositionName, .missingRequiredHeaderField, + .multipartBoundaryTooLong, .missingOrMalformedContentDispositionName, .missingRequiredHeaderField, .missingRequiredMultipartFormDataContentType, .missingRequiredQueryParameter, .missingRequiredPathParameter, .missingRequiredRequestBody, .unsupportedParameterStyle, .failedToParseRequest: .badRequest diff --git a/Sources/OpenAPIRuntime/Multipart/OpenAPIMIMEType+Multipart.swift b/Sources/OpenAPIRuntime/Multipart/OpenAPIMIMEType+Multipart.swift index 4d8b2f25..9594c8bd 100644 --- a/Sources/OpenAPIRuntime/Multipart/OpenAPIMIMEType+Multipart.swift +++ b/Sources/OpenAPIRuntime/Multipart/OpenAPIMIMEType+Multipart.swift @@ -25,6 +25,8 @@ guard let boundary = self.parameters["boundary"] else { throw RuntimeError.missingMultipartBoundaryContentTypeParameter } + // RFC 2046 section 5.1.1 limits the boundary to at most 70 characters. + guard boundary.utf8.count <= 70 else { throw RuntimeError.multipartBoundaryTooLong } return boundary } } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift index a28ec47d..10f48595 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift @@ -131,4 +131,19 @@ final class Test_OpenAPIMIMEType: Test_Runtime { testJSONWith2Params(against: subtypeWildcard, expected: .subtypeWildcard) testJSONWith2Params(against: fullWildcard, expected: .wildcard) } + + func testRequiredBoundaryRejectsBoundaryOverRFC2046Limit() throws { + // RFC 2046 section 5.1.1 limits a multipart boundary to at most 70 characters. + let atLimit = String(repeating: "a", count: 70) + let mimeAtLimit: OpenAPIMIMEType? = OpenAPIMIMEType("multipart/form-data; boundary=\(atLimit)") + XCTAssertEqual(try mimeAtLimit.requiredBoundary(), atLimit) + + let overLimit = String(repeating: "a", count: 71) + let mimeOverLimit: OpenAPIMIMEType? = OpenAPIMIMEType("multipart/form-data; boundary=\(overLimit)") + XCTAssertThrowsError(try mimeOverLimit.requiredBoundary()) { error in + guard case RuntimeError.multipartBoundaryTooLong = error else { + return XCTFail("Unexpected error: \(error)") + } + } + } } From 8e09dca0f81b673cc4cef7a3ef742457840dd823 Mon Sep 17 00:00:00 2001 From: Vladimir Kukushkin Date: Thu, 10 Sep 2026 14:25:44 +0100 Subject: [PATCH 2/2] fixup! Validate multipart boundary length against the RFC 2046 limit --- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index d7619233..d04e386e 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -119,8 +119,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret case .missingRequiredMultipartFormDataContentType: return "Expected a 'multipart/form-data' content type." case .missingMultipartBoundaryContentTypeParameter: return "Missing 'boundary' parameter in the 'multipart/form-data' content type." - case .multipartBoundaryTooLong: - return "Multipart boundary is too long; RFC 2046 allows at most 70 bytes." + case .multipartBoundaryTooLong: return "Multipart boundary is too long; RFC 2046 allows at most 70 bytes." case .transportFailed: return "Transport threw an error." case .middlewareFailed(middlewareType: let type, _): return "Middleware of type '\(type)' threw an error." case .handlerFailed: return "User handler threw an error."