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.
The defect
Section titled “The defect”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: truelookup with b's key returned: "ANSWER-FOR-A"Why it matters
Section titled “Why it matters”Two consequences, and the second is the serious one:
- 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.
- 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.
Affected versions
Section titled “Affected versions”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 fix
Section titled “The fix”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.
How it was found
Section titled “How it was found”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.
Also in this release
Section titled “Also in this release”- sharp in
docs-siteraised 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.
Upgrading
Section titled “Upgrading”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.
docker pull ghcr.io/doublegate/cyberchef-mcp_v2:2.4.1Verification
Section titled “Verification”| 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.