CVE-2023-48230 (published 2023-11-21T21:15Z)

Cap'n Proto is a data interchange format and capability-based RPC system. In versions 1.0 and 1.0.1, when using the KJ HTTP library with WebSocket compression enabled, a buffer underrun can be caused by a remote peer. The underrun always writes a constant value that is not attacker-controlled, likely resulting in a crash, enabling a remote denial-of-service attack. Most Cap'n Proto and KJ users are unlikely to have this functionality enabled and so unlikely to be affected. Maintainers suspect only the Cloudflare Workers Runtime is affected.\n\nIf KJ HTTP is used with WebSocket compression enabled, a malicious peer may be able to cause a buffer underrun on a heap-allocated buffer. KJ HTTP is an optional library bundled with Cap'n Proto, but is not directly used by Cap'n Proto. WebSocket compression is disabled by default. It must be enabled via a setting passed to the KJ HTTP library via `HttpClientSettings` or `HttpServerSettings`. The bytes written out-of-bounds are always a specific constant 4-byte string `{ 0x00, 0x00, 0xFF, 0xFF }`. Because this string is not controlled by the attacker, maintainers believe it is unlikely that remote code execution is possible. However, it cannot be ruled out. This functionality first appeared in Cap'n Proto 1.0. Previous versions are not affected.\n\nThis issue is fixed in Cap'n Proto 1.0.1.1.

CVSS v3 base score: 9.8, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-787
cwe_name: Out-of-bounds Write
description: The product writes data past the end, or before the beginning, of the intended buffer.
extended_description: 
            
            
             
			 
         
         

Fix information:
repo_url: https://github.com/capnproto/capnproto
commit_details: repo_url: https://github.com/capnproto/capnproto
author: Kenton Varda
author_date: 2023-10-04 18:13:16-05:00
msg: Fix inconsistent decision about whether a WebSocket message is compressed.

When processing the header, we decide whether the mesasge is compressed based on the presence of the appropriate flag in the header. However, after receiving the whole message, we were expecting it to be compressed based only on whether the session had negotiated compression upfront. With this patch we only rely on the bits in the header (and throw if the header says it is compressed, but we didn't negotiate compression.)
repository: repo_url: https://github.com/capnproto/capnproto
repo_name: capnproto/capnproto
description: Cap'n Proto serialization/RPC system - core tools and C++ library
repo_language: C++
owner: capnproto
file_changes: filename: http.c++
change_type: ModificationType.MODIFY
diff: @@ -2761,6 +2761,7 @@ public:
     }
 
     bool isFin = recvHeader.isFin();
+    bool isCompressed = false;
 
     kj::Array<byte> message;           // space to allocate
     byte* payloadTarget;               // location into which to read payload (size is payloadLen)
@@ -2771,6 +2772,7 @@ public:
         // Add 4 since we append 0x00 0x00 0xFF 0xFF to the tail of the payload.
         // See: https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.2
         amountToAllocate = payloadLen + 4;
+        isCompressed = true;
       } else {
         // Add space for NUL terminator when allocating text message.
         amountToAllocate = payloadLen + (opcode == OPCODE_TEXT && isFin);
@@ -2819,7 +2821,8 @@ public:
     Mask mask = recvHeader.getMask();
 
     auto handleMessage =
-        [this,opcode,payloadTarget,payloadLen,mask,isFin,maxSize,originalMaxSize,message=kj::mv(message)]() mutable
+        [this,opcode,payloadTarget,payloadLen,mask,isFin,maxSize,originalMaxSize,
+         isCompressed,message=kj::mv(message)]() mutable
         -> kj::Promise<Message> {
       if (!mask.isZero()) {
         mask.apply(kj::arrayPtr(payloadTarget, payloadLen));
@@ -2838,8 +2841,10 @@ public:
           KJ_UNREACHABLE;
         case OPCODE_TEXT:
 #if KJ_HAS_ZLIB
-          KJ_IF_SOME(config, compressionConfig) {
+          if (isCompressed) {
+            auto& config = KJ_ASSERT_NONNULL(compressionConfig);
             auto& decompressor = KJ_ASSERT_NONNULL(decompressionContext);
+            KJ_ASSERT(message.size() >= 4);
             auto tail = message.slice(message.size() - 4, message.size());
             // Note that we added an additional 4 bytes to `message`s capacity to account for these
             // extra bytes. See `amountToAllocate` in the if(recvHeader.isCompressed()) block above.
@@ -2862,8 +2867,10 @@ public:
           return Message(kj::String(message.releaseAsChars()));
         case OPCODE_BINARY:
 #if KJ_HAS_ZLIB
-          KJ_IF_SOME(config, compressionConfig) {
+          if (isCompressed) {
+            auto& config = KJ_ASSERT_NONNULL(compressionConfig);
             auto& decompressor = KJ_ASSERT_NONNULL(decompressionContext);
+            KJ_ASSERT(message.size() >= 4);
             auto tail = message.slice(message.size() - 4, message.size());
             // Note that we added an additional 4 bytes to `message`s capacity to account for these
             // extra bytes. See `amountToAllocate` in the if(recvHeader.isCompressed()) block above.

programming_language: C