v2.4.0 release notes
Release Date: 2026-09-01
Upstream Base: CyberChef v11.4.0 (unchanged)
Licence: GPL-3.0-or-later
Node: >=24 <27
Highlights
Section titled “Highlights”Every tool this server has ever exposed came from OperationConfig — a pure run(input, args)
over one input. That shape cannot express an analysis: scoring dozens of candidate key lengths,
factoring a modulus four different ways, or composing several operations and comparing the results.
cyberchef_bake does not close the gap either, because a recipe is a linear pipeline, not a loop.
v2.4.0 adds the registry those tools need, and four tools on it.
| Tool | Replaces | What an operation could not do |
|---|---|---|
cyberchef_xor_key_length |
xortool |
Score every candidate key length and rank them |
cyberchef_cyclic_pattern |
pwntools cyclic |
Search a generated pattern for a fragment |
cyberchef_hash_identify |
hashid, hash-identifier |
Match a hash against a table of structures |
cyberchef_rsa_attack |
RsaCtfTool |
Try four attacks and report which one applied |
The second theme is less pleasant and more useful: three documents in this repository described work that had not been done. All three are corrected below.
The registry, and the loader that is not being built
Section titled “The registry, and the loader that is not being built”src/node/tools/registry.mjs holds tools that are not operations. Registration enforces a
lower-snake-case name, a title, a description, a category, a Zod object schema and a run
function — and one rule that matters more than the rest:
A registry tool can never shadow a CyberChef operation. Registration throws on a collision with
any of the 504 operation tools or any meta-tool, rather than resolving it by import order. Without
that, which cyberchef_aes_decrypt you got would depend on module load sequence, and the loser
would be an operation callers already trust.
Loading is src/node/tools/index.mjs: an explicit array of imports. No directory scan, no glob, no
import() of a path from configuration. That is deliberate, and the roadmap’s “plugins with
sandboxed execution” line is the reason it needs stating.
node:vm is not a security boundary, and this was measured rather than assumed:
const ctx = vm.createContext({ bake });vm.runInContext("bake.constructor('return process')()", ctx); // the real processA function handed into a vm context carries a constructor that closes over the host realm.
Every useful tool needs at least one capability, so the “narrow API” defence is unavailable by
construction. ADR 0002 records the
measurement and the conditions under which the decision should be revisited: process isolation
plus an explicit capability allowlist, not a tidier vm — and specifically not a worker thread,
which bounds CPU rather than authority and shares the process’s filesystem, network and
environment.
Four tools, each pinned to a known answer
Section titled “Four tools, each pinned to a known answer”cyberchef_xor_key_length
Section titled “cyberchef_xor_key_length”Recovers the length of a repeating-key XOR by index of coincidence, then guesses the key and decrypts. Ranked candidates with scores, and a confidence figure relative to random.
The obvious scoring function is chi-square, and it is wrong: chi-square grows with sample size, so it ranks short key lengths highest regardless of the data. The first implementation answered “1” for every input and looked entirely plausible doing it. IC is normalised by construction. (Findings log F-02.)
cyberchef_cyclic_pattern
Section titled “cyberchef_cyclic_pattern”Generates a De Bruijn pattern and finds the offset of a fragment within one — how you locate the
return-address bytes in a stack overflow. Byte-compatible with pwntools’ cyclic, which is the
entire point: an offset found here has to equal the one a colleague found with cyclic -l. Pinned
to pwntools’ canonical output.
Two decisions worth knowing:
- A hex fragment is read as both endiannesses and both offsets are returned when both match. A crash dump rarely tells you which it is, and silently picking one hands back a plausible wrong number — the worst available failure mode for a tool whose whole output is a number.
- Generating a pattern longer than the alphabet can keep unique is refused, not truncated.
Past
k^nbytes the windows repeat, and a repeated window makes every offset ambiguous.
cyberchef_hash_identify
Section titled “cyberchef_hash_identify”Identifies a hash by structure — bcrypt, sha512crypt, argon2, PHPass, Django, LDAP, MySQL, NetNTLM and more — and returns the hashcat mode and John format name, so the output is a command you can run.
This fills a real gap rather than a theoretical one. CyberChef computes around forty digests and
cannot tell you what one is; its Analyse hash operation reads hex length only, and reports
Invalid hash for bcrypt, sha512crypt and argon2 — precisely the formats you are most likely to be
holding.
For bare hex the answer can only come from length, and the tool says so: 32 hex characters is MD5,
NTLM, MD4, LM and RIPEMD-128, confidence is length only and ambiguous is true.
cyberchef_rsa_attack
Section titled “cyberchef_rsa_attack”Tests a public key for the four generation flaws that make it breakable, and recovers the private key when one applies.
| Attack | The flaw it detects |
|---|---|
| Fermat | p and q too close — a generator that picked one prime and searched upward |
| Common factor | a prime shared with a second modulus — low-entropy pool at first boot. One gcd breaks both keys |
| Wiener | a private exponent chosen small to make decryption fast |
Small e, unpadded |
e=3 with a message short enough that m^e never wrapped the modulus |
None of these threatens a correctly generated key. A sound 2048-bit modulus defeats all four, quickly and by design. So a negative result is reported as four flaws ruled out and explicitly not as evidence the key is strong — the report says so in as many words, because a tool that implies otherwise is worse than one that says nothing.
CyberChef can encrypt, decrypt, sign, verify and generate RSA keys, and had no way to assess one.
Three documents that described work nobody had done
Section titled “Three documents that described work nobody had done”THIRD-PARTY-NOTICES.md credited eight ports that were not ports
Section titled “THIRD-PARTY-NOTICES.md credited eight ports that were not ports”The reference-tool section said eight projects had been “incorporated in v2.0.0”, and that each ported file carried a provenance comment naming the source project, file and commit. Neither half was true. What these tools take is a wire format, an algorithm choice or an identifier table — and four of the eight contributed nothing at all, because measuring them showed the capability was already present:
| Project | Why nothing was taken |
|---|---|
| katana | Its useful core is auto-decode plus a flag regex. Magic already does the first; the second is one recipe. |
| Ciphey / Ares | Auto-decode search — the same idea as Magic, already exposed. |
| cryptii | Its encodings have 26 equivalents among the 504 operations. |
katana remains the reason this project is GPLv3 rather than v2. That decision was taken before this was measured and is not being reversed; it is recorded so the reasoning is not mistaken for a current dependency.
The cyberchef-recipes note had the same shape: it described a 71-preset corpus as revalidated
against v11.4.0 and reworked into parameterised tools. None of it has been built. The scope
decision is worth keeping — it settles the licensing question in advance — so it is retained,
rewritten in the future tense it should always have had.
The generated tool reference omitted four tools while promising it could not
Section titled “The generated tool reference omitted four tools while promising it could not”The docs site generates its reference from the same OperationConfig.json the server reads, and
says so on the page: it “cannot disagree with the running server about what exists”. Registry tools
are not in OperationConfig, so all four were silently absent from a page making exactly that
claim. A reference that promises completeness and then leaves things out is worse than one that
promises nothing. docs-site/scripts/collect.mjs now reads the registry the way the server does,
and emits an Analysis category alongside the operation modules.
A known-answer test whose known answer was wrong
Section titled “A known-answer test whose known answer was wrong”hash_identify failed on a sha512crypt hash. The pull on a red known-answer test is to widen the
pattern — and the pattern was right. sha512crypt’s digest is exactly 86 characters and the vector
I had written from memory carried 84:
digest len in my test vector: 84Replaced with a hash generated on the spot (openssl passwd -6 -salt usesomesillystri password),
which matched immediately. Recorded as F-07 because the failure mode is not the typo, it is the
reflex: generate the oracle or cite where it came from, never recall it. Roughly half the time,
a red known-answer test means the answer is wrong.
The bound that looked like a bound, and was not
Section titled “The bound that looked like a bound, and was not”The first-pass reviewer on PR #100 pointed at the schemas, and it was right. Each of the four tools
bounds its numeric arguments carefully — fermat_iterations at 10,000,000, max_key_length at 256,
length at 1 MB. Not one string argument had a limit. The schemas look validated, which is what
made the gap easy to miss: the obvious fields are all capped.
Measuring settled which bound actually matters, and it was not the one already there:
fermat, 1,000,000 iterations, 65-bit modulus 582 msfermat, 100 iterations, 262,144-bit modulus 72,125 msThe cost is in the size of the numbers, not the iteration count. So fermat_iterations — the
argument that looks like the safety bound — bounded nothing: 100 iterations against a 64 KB hex
string blocked the event loop for 72 seconds, from a single call, on a server that holds every
operation tool to a 30-second timeout. xor_key_length had the same shape with a gentler slope:
O(input x max_key_length), so 1 MB costs 3.2 s and the server’s general 100 MB ceiling would be
about five minutes.
Fixed in four parts, in order of how much each matters:
- A bound on every string argument, each with a stated reason.
MAX_OPERAND_CHARS = 5000admits a 16,384-bit key as hex or decimal;xor_key_lengthtakes 1 MB, measured. - A bit-length guard behind it, because the character limit is only a proxy — 4,990 decimal digits is 16,577 bits, inside one bound and outside the other.
- The operation timeout, applied to registry tools, with retries off: retrying a timed-out analysis repeats exactly the work that caused the timeout.
- A cooperative yield in the Fermat loop, which is what makes (3) mean anything. A synchronous
loop cannot be timed out —
Promise.racenever gets a turn, so the timeout would resolve only after the work it was meant to bound had already finished.
Verified end to end through a real client: 72,125 ms → 2 ms, returned as a structured
INVALID_INPUT, with a genuine RSA-4096 modulus still accepted and still analysed.
A second review, from Copilot, found the bound that F-09’s own fix had missed. integerRoot
computes hi ** k where k is the caller’s public exponent, and raising to a huge power is fatal
rather than slow: a 400-digit exponent — comfortably inside the new 5,000-character limit — returns
RangeError: Maximum BigInt size exceeded instead of an answer. The obvious fix is wrong, which is
the interesting part: a large e is exactly the signature Wiener’s attack looks for, so
capping e globally would disable the attack most likely to succeed. The guard sits on the
small-e attack alone, which is meaningful only for e = 3 and occasionally 5 or 17, and a skip is
reported in attempted rather than silently omitted.
A third round measured the bound at its own ceiling, and found it still four orders of magnitude too loose. 16,384 bits is what the size guard deliberately permits, and there:
fermat, 1,000 iterations 21,780 msfermat, 10,000 iterations 223,909 ms (the default is 100,000 -> about 37 minutes)The yield does not save that, and why is the useful part: Promise.race does not cancel the
loser. The timeout fires, the caller gets an error, and the loop runs on — so a client that
times out repeatedly accumulates runaway searches behind its own error responses, while the server
stays responsive and nothing looks wrong.
Two fixes. isqrt started Newton’s method at n, which merely halves each step until it nears the
root — about 8,000 big-integer divisions for a 16,384-bit modulus, once per Fermat iteration
through isPerfectSquare; starting at 2^(bits/2+1) is already within a factor of two, and made
each iteration ~187x cheaper. And the loop now checks a ten-second deadline itself, so the work
stops rather than being abandoned by a caller who has gone away, and reports
fermat (stopped at the 10s limit after 83,199 of 100,000 iterations) — because “found nothing”
would be a claim about a search that never finished. Worst case after: 10,002 ms.
xor_key_length’s scan was already bounded, but allocated one array per column — up to 32,896 of
them holding a megabyte — for a statistic that needs only the counts. Reading in place took 1 MB
from 3,213 ms to 594 ms, with the recovered key length unchanged.
The lesson is worth more than the fix. The earlier bound was chosen by reasoning about what an RSA key is — “16,384 bits is beyond anything in use, so refusing above it costs no capability”. True, and irrelevant: the question is not which inputs are legitimate but what the worst legitimate input costs. A bound argued from the domain still has to be measured at its own ceiling.
Two claims across these reviews did not survive checking. hash_identify’s hex fallback was flagged as
able to “hang on massive inputs”; it cannot. Every pattern is anchored over a single character
class, so there is no backtracking to exploit, and 8 MB of junk costs 56 ms. It is bounded anyway,
at 4 KB — because no hash is 8 MB, not because it is slow. And wiener was flagged as risking
event-loop starvation with roughly 11,000 convergents at 16,384 bits; the convergent count is
bounded by the Euclidean chain rather than the bit length, and measured 2 ms on that modulus.
The cost of the default surface, stated
Section titled “The cost of the default surface, stated”The four tools are exposed at every surface, including the default index, because each replaces
a separate command-line tool and none is reachable through cyberchef_bake. That is not free:
CYBERCHEF_TOOL_SURFACE |
tools | payload | tokens |
|---|---|---|---|
index (default) |
24 → 28 | 19,492 bytes | ~3.4k → ~4.9k |
curated |
102 → 106 | 82,738 bytes | ~19.2k → ~20.7k |
all |
527 → 531 | 399,896 bytes | ~98.5k → ~100k |
Measured on the serialised tools/list payload from a real client, not estimated. ~1.5k tokens on
every request is the price of four always-on tools with real argument documentation, and it is
stated here rather than left for someone to discover.
Upgrading
Section titled “Upgrading”No action required. Nothing in this release is breaking:
- No tool was renamed, removed, or given different arguments.
- The four new tools are additive at every surface.
- No protocol, transport, or configuration behaviour changed.
Verification
Section titled “Verification”docker pull ghcr.io/doublegate/cyberchef-mcp_v2:2.4.0Then point a real MCP client at it. That is the authoritative check, for this project’s own most
expensive reason: raw JSON-RPC does no schema validation, so three releases once shipped with every
tool carrying an empty inputSchema while hand-written probes reported success.
{ "mcpServers": { "cyberchef": { "command": "docker", "args": ["run", "-i", "--rm", "ghcr.io/doublegate/cyberchef-mcp_v2:2.4.0"] } } }Calling cyberchef_hash_identify with
$2b$12$GhvMmNVjRW29ulnudl.LbuAnUtN/LRfe1JsBm1Xu6LE3059z5Tr8m returns:
{ "identified": true, "most_likely": { "format": "bcrypt", "confidence": "structural", "hashcat_mode": 3200, "john_format": "bcrypt", "note": "Cost is the number after the second $: 10 means 2^10 rounds." }, "ambiguous": false, "next": "hashcat -m 3200"}| Gate | Result |
|---|---|
| MCP tests | 1,111 across 39 files |
| Node API tests | 241 |
| Operation tests | 2,289 |
| Coverage | 95.67% statements / 89.16% branches / 96.43% functions / 96.50% lines |
| Lint | clean |
| Client contract | all four tools present with populated schemas at index, curated and all |
| Docs site | 71 pages build, including the generated Analysis reference |
npm audit --omit=dev |
4 low, all the same transitive elliptic advisory reached through crypto-browserify. Unchanged since v2.3.0; carried knowingly rather than silently. |
Full working record, including what was measured and what the plan got wrong:
docs/internal/v2.4.0-findings-log.md.