From 2956de17a0d99990a80f24c459ae71354726facc Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Wed, 2 Sep 2026 17:51:03 +1000 Subject: [PATCH 1/2] docs(security): document the bounded decompression path (LAB-2504) SECURITY.md covered reporting and scope but said nothing about the read pipeline, so a reader had no way to tell whether this SDK decompresses anything itself. It does not: both bindings (NAPI and the Workers wasm build) are thin wrappers over cachekit-core's extract(), which bounds output at min(512 MiB, 1000 x compressed_len) before decompressing, does not trust the envelope's declared original_size, and does not rely on the unkeyed xxHash3-64 checksum for anything but corruption detection. Calls out that maxDecodedSize is checked on already-decompressed bytes, so it is downstream of that bound rather than a substitute for it. Flags the Workers case specifically: 512 MiB is a server-class ceiling and an isolate has roughly 128 MiB, so on that build a payload well inside core's limits can still exhaust it. Sizing belongs to LAB-2505; this records the constraint so nobody reads the ceiling as tuned for Workers. --- SECURITY.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/SECURITY.md b/SECURITY.md index 241a74e..9dd99db 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -17,3 +17,22 @@ Instead, use [GitHub's private vulnerability reporting](https://github.com/cache ## Scope This policy covers the `@cachekit-io/cachekit` and `@cachekit-io/cachekit-core-ts` packages. For issues with the CacheKit SaaS platform (api.cachekit.io), contact security@cachekit.io. + +## Bounded decompression + +This SDK does not implement LZ4. `ByteStorage.unpack` in both bindings — +`cachekit-core-ts` (NAPI, native) and `cachekit-core-wasm` (Workers) — is a thin +wrapper over cachekit-core's `ByteStorage::retrieve` → `StorageEnvelope::extract`, +which bounds the decompressed output at `min(512 MiB, 1000 × compressed_len)` +_before_ decompressing. The envelope's self-declared `original_size` is not +trusted, and the xxHash3-64 checksum is unkeyed so it does not gate a forging +attacker — see [cachekit-core: Decompression limits](https://github.com/cachekit-io/cachekit-core/blob/main/SECURITY.md#decompression-limits). + +`serializer.maxDecodedSize` is checked on the already-decompressed bytes, so it +sits downstream of that bound rather than replacing it. + +> [!IMPORTANT] +> The 512 MiB ceiling is server-class. A Cloudflare Workers isolate has roughly +> 128 MiB, so on the Workers build a payload well inside cachekit-core's limits +> can still exhaust the isolate. Bound payload size at the caller when running +> on Workers. Making the ceiling environment-aware is tracked in LAB-2505. From c8aebfb47df632c4c5cc7ad1ac04d159c7651679 Mon Sep 17 00:00:00 2001 From: Ray Walker Date: Wed, 2 Sep 2026 18:07:35 +1000 Subject: [PATCH 2/2] docs(security): name the real ceiling and the real lever (LAB-2504) Expert-panel findings on the previous commit. 'maxDecodedSize is checked on the already-decompressed bytes' was true but buried the consequence: the default is 10 MiB and core will materialize up to 512 MiB before it is ever consulted, so the two ceilings differ by ~51x and turning maxDecodedSize down to harden a Workers deployment does nothing to what unpack may allocate. cache-core.ts:574 currently claims the blast radius IS bounded by maxDecodedSize, which is wrong by that same factor; filed as LAB-2732 and cross-referenced here. The Workers callout also said 'bound payload size at the caller' without naming a lever, while the only knob the section mentions is the one that does not work -- so a reader would reasonably reach for maxDecodedSize and stay exposed. Now names the two levers that exist: check the fetched byte length before handing it to the cache, or cap value size at the backend. --- SECURITY.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 9dd99db..5341039 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -28,11 +28,17 @@ _before_ decompressing. The envelope's self-declared `original_size` is not trusted, and the xxHash3-64 checksum is unkeyed so it does not gate a forging attacker — see [cachekit-core: Decompression limits](https://github.com/cachekit-io/cachekit-core/blob/main/SECURITY.md#decompression-limits). -`serializer.maxDecodedSize` is checked on the already-decompressed bytes, so it -sits downstream of that bound rather than replacing it. +`serializer.maxDecodedSize` (10 MiB by default) is checked inside +`serializer.decode`, i.e. on the already-decompressed bytes. It sits downstream +of core's bound rather than replacing it, so the two ceilings differ by ~51x: +core will materialize up to 512 MiB before `maxDecodedSize` is ever consulted. +Raising or lowering `maxDecodedSize` does not change what `unpack` may allocate. +Tracked in LAB-2732. > [!IMPORTANT] > The 512 MiB ceiling is server-class. A Cloudflare Workers isolate has roughly > 128 MiB, so on the Workers build a payload well inside cachekit-core's limits -> can still exhaust the isolate. Bound payload size at the caller when running -> on Workers. Making the ceiling environment-aware is tracked in LAB-2505. +> can still exhaust the isolate. This SDK has no read-side pre-decompression +> bound, so the only lever is to check the fetched value's byte length yourself +> before handing it to the cache, or to cap value size at the backend. Making +> core's ceiling environment-aware is tracked in LAB-2505.