Skip to content

v2.4.1 release notes

Release Date: 2026-09-02 Upstream Base: CyberChef v11.4.0 (unchanged) Licence: GPL-3.0-or-later Node: >=24 <27

A security release. Upgrade, or disable the cache.

LRUCache.getCacheKey hashed only the first 1,000 characters of the input:

hash.update(input.substring(0, 1000)); // "Use first 1KB for hash"

Two different inputs sharing that prefix therefore produce the same key, and the second caller receives the first caller’s answer. Reproduced against the shipped code:

a = "x".repeat(1000) + "SECRET-A" (1,008 chars)
b = "x".repeat(1000) + "DIFFERENT-B" (1,011 chars)
keys equal: true
lookup with b's key returned: "ANSWER-FOR-A"

Two consequences, and the second is the serious one:

  1. A silently wrong result for valid input. The caller receives output computed from data they never sent, with no error and nothing to indicate it.
  2. Cross-caller data exposure on a shared server. Where several clients share one instance — the HTTP transport, which exists for exactly that — caller B can receive the output of an operation performed on caller A’s input.

This is the same shape as the pooled-buffer defect fixed in v2.3.0: a caller receiving bytes that were never theirs, reached by a different route.

Nothing about triggering it is exotic. The cache is on by default. Inputs over 1,000 characters are ordinary. A shared 1,000-character prefix is the normal case for the same document with different trailing content, for log lines, for padded records, for anything with a header. No malice is required; two honest callers are enough.

v1.4.0 through v2.4.0 — introduced with the LRU cache in v1.4.0 and present in every release since, including the whole of the v2.x line and the supported v1.9.x maintenance line.

Patched v2.4.1
Mitigation without upgrading CYBERCHEF_CACHE_ENABLED=false
Not affected in practice Single-user stdio deployments — the correctness half applies, but there is no second caller to receive the data

The whole input is hashed, plus its length:

hash.update(String(input.length));
hash.update(input);

The cost was measured, not assumed. Full SHA-256 is 2.3 ms at 1 MB and 252 ms at the 100 MB input ceiling. That is negligible against the operation the cache guards, which scales with the same input — Gzip alone is 305 ms at 100 KB, so at 100 MB the operation dominates the hash by orders of magnitude.

A cache that returns the wrong answer quickly is worth less than no cache at all.

The length is mixed in as well. It is redundant given a full hash, and it is the one line that makes a prefix collision impossible to reintroduce by “optimising” the content hash back to a substring without also noticing the length.

While scoping multi-tenancy for v2.5.0 — specifically, asking what would need isolating between tenants. Cache sharing turns out to be safe for deterministic pure operations: the same input yields the same output, which the caller already has. That made the real question “does the key actually identify the input?”, and the answer was no.

The bug had survived since v1.4.0 through a decomposition refactor, six releases, and a coverage gate raised to 95/88/96/96. Every test passed because every test used short inputs.

  • sharp in docs-site raised to ^0.35.3, resolving 0.35.4 — clears GHSA-f88m-g3jw-g9cj and four inherited libvips CVEs.
  • postcss-selector-parser raised to 7.1.5 — clears GHSA-w9m9-85wc-3x92 (ReDoS). Transitive and dev-only, through css-loader, which this fork does not ship.

No action beyond upgrading. Nothing is breaking. Cache entries are keyed differently, so the cache starts cold after the upgrade — that is the intended effect, since the old entries were keyed by a value that did not identify their input.

Terminal window
docker pull ghcr.io/doublegate/cyberchef-mcp_v2:2.4.1
Gate Result
MCP tests 1,115 across 39 files, including four new regression tests
Node API tests 241
Operation tests 2,289
Lint clean
Benchmarks unchanged — they measure raw operations, not the cache path

The regression test is the reproduction above, asserted directly: two inputs sharing a 1,000-character prefix must not share a key, and a lookup with one must not return the other’s value.