diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 6ea5caf5..d04e386e 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,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 .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 +168,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)") + } + } + } }