Files
cpp-core/security-advisories/2023-11-21-0-websocket-compression-dos.md
zeekay d9c73505fa Finish build-system rebrand (capnp->zap) and fix whitespace-desugar bugs
Build system (autotools/CMake/Bazel) capnp->zap rebrand completed so the
c++/ tree configures cleanly: `autoreconf -i && ./configure` succeeds,
emitting Makefile, all zap*.pc / kj*.pc and the Zap CMake config. configure.ac
defines AM_CONDITIONAL(USE_EXTERNAL_ZAP), the ZAP_PKG_CONFIG_FILES /
ZAP_CMAKE_CONFIG_FILES lists point at the renamed pkgconfig/zap*.pc.in and
cmake/Zap*.cmake.in, and Makefile.am's `if USE_EXTERNAL_ZAP` matches. Bazel
loads @zap-cpp//src/zap:cc_zap_library.bzl. Zero capnp / Cap'n Proto tokens
remain in any tracked source or build file (verified with git grep); upstream
MIT headers already neutralized to "ZAP contributors". quick-test.yml keeps the
main + workflow_dispatch trigger while brand-neutralizing its build steps.

Whitespace-significant schema desugar (compiler/desugar.c++) — fix three HIGH
correctness bugs and make the preprocessor total:

  * Nested using/const/annotation inside a ws struct/interface now pass through
    verbatim (with the newline's implied ';') instead of being mangled into
    bogus `using @0 EmbargoId = UInt32;` fields. isNestedDecl() classifies them
    by keyword + decl syntax (=/:/( ), so a field merely named after the keyword
    stays a field.
  * Ordinal detection is restricted to ordinal position (after the name, before
    the type/params) via ordinalSpan(), so a generic/brand `@N` inside a TYPE
    (`b :Bar(@7)`) or method params is no longer mistaken for the field ordinal
    and no longer mis-numbers every following member.
  * Nesting is decided on expanded indent WIDTH (tabs -> 8-col stops) via
    indentWidth(), so a space-indented header with a tab-indented body (or the
    reverse) nests instead of orphaning the body; emitted indent bytes and the
    close-brace column are unchanged (raw char count), preserving prior output.
  * Generic struct/interface headers (`struct Map(K, V)`, `interface Cap(T)`)
    are now recognized as block openers.
  * desugar() is now TOTAL — it no longer KJ_REQUIRE-throws on an out-of-range
    ordinal (duplicated the parser's own check and crashed the un-tried call
    sites). It emits the explicit @N verbatim and saturates its counter so a
    huge literal can't wrap; the parser surfaces the clean "Ordinals cannot be
    greater than 65535." diagnostic at the right file:line:col. Removed the now
    redundant validateAutoOrdinal and the dead kj/debug.h include. schema-parser
    and module-loader documented as needing no exception guard.

desugar-test.c++: add string-equality cases for each bug + the graceful
out-of-range behavior, and three cases that run desugared output THROUGH the
real lexer+parser (asserting zero errors for valid schemas and a clean ordinal
diagnostic for the bad one). Brace back-compat proven byte-identical over the
18-file corpus; sample.ws.zap still desugars to sample.brace.zap exactly.
2026-06-20 00:02:20 -07:00

4.0 KiB

Problem

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 Zap and KJ users are unlikely to have this functionality enabled and so unlikely to be affected. We suspect only the Cloudflare Workers Runtime is affected.

Discovered by

Cloudflare Workers team

Announced

2023-11-21

CVE

CVE-2023-48230

Impact

  • If 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 Zap, but is not directly used by Zap. If you haven't heard of it, you probably aren't using it.
  • WebSocket compression is disabled by default. It must be enabled via a setting passed to the KJ HTTP library via HttpClientSettings or HttpServerSettings. We suspect no one uses this setting today except for Cloudflare Workers.
  • 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, we suspect it is unlikely that remote code execution is possible. However, it cannot be ruled out.
  • This functionality first appeared in Zap 1.0. Previous versions are not affected.

Fixed in

C++ fix:

Details

KJ is a C++ toolkit library that evolved along with the Zap C++ implementation. While it originally contained only a few utilities used by Zap itself, it has grown over time with quite a few features. One of the larger features is an HTTP/1.1 implementation, complete with WebSocket support. KJ's HTTP library is used by the Cloudflare Workers runtime.

In 2022, the Cloudflare Workers team added support for the WebSocket compression extension to KJ HTTP. The feature is enabled by default for Workers whose compatibility date is 2023-08-15 or later. That is, this feature only became enabled by default recently, and only for new Workers or Workers that updated their compatibility date.

A bug in the code caused it to improperly handle the case where compression had been negotiated, but a particular message received from the peer was not compressed, i.e. did not have the compression bit set in the frame header. The code would correctly allocate buffer space for a non-compressed message. However, later on, the code incorrectly treated the message as compressed, and therefore attempted to prefix it with the constant string { 0x00, 0x00, 0xFF, 0xFF } which must be added before passing the bytes to zlib. Since the buffer was not allocated with space for these bytes, they would be written into the four bytes preceding the buffer instead.

In most cases, this had the effect of overwriting state maintained by the memory allocator, causing a subsequent memory allocation operation to crash.

Because the bytes are always a constant string, not attacker-controlled, it seems difficult to leverage this overrun to achieve code execution. However, we cannot rule out an excessively clever exploit.

The bug was discovered when it was triggered in production, and was quickly patched. The occurrences in production were benign, not an attack. It is normal for some WebSocket implementations to negotiate compression but then opportunistically skip it when it doesn't help, which would trigger the bug.