Browse Source

Fix decompression of empty messages with a ratio limit (#2246)

Motivation:

The decompressor has a decompression limit to protect against zip bombs.
This can either be absolute or ratio based. It's also possible in gRPC
for a zero length message to be marked as compressed. gRPC attempts to
decompress the zero length message and fails (because zlib wants a
non-zero sized buffer and gRPC won't give it one as the limit is the
buffer size is limited by the `ratio * msg_size` which in this case is
zero).

Modifications:

- If the input to decompress has no length, skip decompression
altogether

Result:

- Can decompress zero length payloads with the ratio limit
- Resolves #2245
George Barnett 7 months ago
parent
commit
a56a157218
2 changed files with 18 additions and 0 deletions
  1. 6 0
      Sources/GRPC/Compression/Zlib.swift
  2. 12 0
      Tests/GRPCTests/ZlibTests.swift

+ 6 - 0
Sources/GRPC/Compression/Zlib.swift

@@ -265,6 +265,12 @@ enum Zlib {
     /// - Returns: The number of bytes written into `output`.
     @discardableResult
     func inflate(_ input: inout ByteBuffer, into output: inout ByteBuffer) throws -> Int {
+      if input.readableBytes == 0 {
+        // Zero length compressed messages are always empty messages. Skip the inflate step
+        // below and just return the number of bytes we wrote.
+        return 0
+      }
+
       return try input.readWithUnsafeMutableReadableBytes { inputPointer -> (Int, Int) in
         // Setup the input buffer.
         self.stream.availableInputBytes = inputPointer.count

+ 12 - 0
Tests/GRPCTests/ZlibTests.swift

@@ -207,4 +207,16 @@ class ZlibTests: GRPCTestCase {
     let ratio: DecompressionLimit = .ratio(2)
     XCTAssertEqual(ratio.maximumDecompressedSize(compressedSize: 10), 20)
   }
+
+  func testDecompressEmptyPayload() throws {
+    for limit in [DecompressionLimit.ratio(1), .absolute(1)] {
+      for format in [Zlib.CompressionFormat.deflate, .gzip] {
+        var compressed = self.allocator.buffer(capacity: 0)
+        let inflate = Zlib.Inflate(format: format, limit: limit)
+        var output = self.allocator.buffer(capacity: 0)
+        XCTAssertEqual(try inflate.inflate(&compressed, into: &output), 0)
+        XCTAssertEqual(output.readableBytes, 0)
+      }
+    }
+  }
 }