From 534032f4fe842062c0a64117763552b43a23a612 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:35:10 +0100 Subject: [PATCH 01/13] Fix, and link, Fernet tests (#2335) --- tests/operations/index.mjs | 1 + tests/operations/tests/Fernet.mjs | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/operations/index.mjs b/tests/operations/index.mjs index 19637e34..bb81d32e 100644 --- a/tests/operations/index.mjs +++ b/tests/operations/index.mjs @@ -72,6 +72,7 @@ import "./tests/ExtractAudioMetadata.mjs"; import "./tests/ExtractEmailAddresses.mjs"; import "./tests/ExtractHashes.mjs"; import "./tests/ExtractIPAddresses.mjs"; +import "./tests/Fernet.mjs"; import "./tests/Float.mjs"; import "./tests/FileTree.mjs"; import "./tests/FletcherChecksum.mjs"; diff --git a/tests/operations/tests/Fernet.mjs b/tests/operations/tests/Fernet.mjs index ee9ba2f1..7a17a675 100644 --- a/tests/operations/tests/Fernet.mjs +++ b/tests/operations/tests/Fernet.mjs @@ -5,7 +5,7 @@ * @copyright Karsten Silkenbäumer 2019 * @license Apache-2.0 */ -import TestRegister from "../TestRegister"; +import TestRegister from "../../lib/TestRegister.mjs"; TestRegister.addTests([ { @@ -47,7 +47,7 @@ TestRegister.addTests([ { name: "Fernet Encrypt: no input", input: "", - expectedMatch: /^gAAAAABce-[\w-]+={0,2}$/, + expectedMatch: /^gAAA[\w-]+={0,2}$/, recipeConfig: [ { op: "Fernet Encrypt", @@ -69,12 +69,27 @@ TestRegister.addTests([ { name: "Fernet Encrypt: valid arguments", input: "This is a secret message.\n", - expectedMatch: /^gAAAAABce-[\w-]+={0,2}$/, + expectedMatch: /^gAAA[\w-]+={0,2}$/, recipeConfig: [ { op: "Fernet Encrypt", args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="] } ], + }, + { + name: "Fernet Encrypt/Decrypt: round trip", + input: "This is a secret message.\n", + expectedOutput: "This is a secret message.\n", + recipeConfig: [ + { + op: "Fernet Encrypt", + args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="] + }, + { + op: "Fernet Decrypt", + args: ["MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI="] + }, + ], } ]); From 09bb086853c16190f5f8adcec74435e02b50c8ce Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Mon, 27 Apr 2026 12:44:08 +0100 Subject: [PATCH 02/13] Update CONTRIBUTING.md (#2333) --- CONTRIBUTING.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abb37d42..0cee9a7b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,10 +6,13 @@ There are lots of opportunities to contribute to CyberChef. If you want ideas, t Before your contributions can be accepted, you must: - - Sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef) + - Fork the CyberChef repo + - Create a new branch within your fork for the changes - Push your changes to your fork. + - Sign the [GCHQ Contributor Licence Agreement](https://cla-assistant.io/gchq/CyberChef) - Submit a pull request. +Please note that we will ***reject*** pull requests from the master branch of your fork owing to the mess it makes of our own working repositories and the extra work entailed. ## Coding conventions From 9088561acf75fce6961efda2ed276f3f15b9bf04 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Mon, 27 Apr 2026 13:17:39 +0100 Subject: [PATCH 03/13] Bump v10.24.0 (#2338) --- CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f9d7969..0a8a0b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,30 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [10.24.0] - 2026-04-27 +- Update CONTRIBUTING.md [@GCHQDeveloper581] | [#2333] +- Fix, and link, Fernet tests [@GCHQDeveloper581] | [#2335] +- [#927] added parity bit operation [@j83305] | [#1036] +- Feature md link blanks [@BjoernAkAManf] [@GCHQDeveloper581] | [#660] +- Accessibility - Add support for screenreaders in operations search [@mattnotmitt] | [#1862] +- Added metadata extraction for UUID strings. [@ko80240] | [#2322] +- chore (deps): bump the patch-updates group with 6 updates | [#2330] +- chore (deps): bump @codemirror/search from 6.6.0 to 6.7.0 | [#2331] +- (Feature) Improve CI [@GCHQDeveloper581] | [#2328] +- Update dependabot.yml [@GCHQDeveloper581] | [#2326] +- chore (deps): bump lodash, grunt-legacy-log and grunt-legacy-util | [#2327] +- chore (deps): bump the patch-updates group with 6 updates [@GCHQDeveloper581] | [#2323] +- chore (deps): bump autoprefixer from 10.4.27 to 10.5.0 | [#2324] +- chore (deps): bump dompurify from 3.3.3 to 3.4.0 | [#2321] +- chore (deps): bump follow-redirects from 1.15.11 to 1.16.0 | [#2320] +- Regular Expression operation email address regex: Support IPv4 domains [@C85297] [@GCHQDeveloper581] | [#2167] +- Rewriting fixCryptoApiImports and fixSnackbarMarkup to js to make it OS agnostic [@BigYellowHammer] | [#2298] +- chore (deps): bump basic-ftp from 5.2.1 to 5.2.2 | [#2317] +- chore (deps): bump axios from 1.13.6 to 1.15.0 | [#2316] +- chore (deps): bump webpack from 5.105.4 to 5.106.0 | [#2315] +- chore (deps): bump basic-ftp from 5.2.0 to 5.2.1 | [#2313] +- Update vulnerable dependencies [@GCHQDeveloper581] | [#2311] + ### [10.23.0] - 2026-04-06 - Properly escape HTML entities in sampleDelim to avoid XSS issue [@GCHQDeveloper581] | [#2307] - chore (deps): bump lodash from 4.17.23 to 4.18.1 | [#2304] @@ -596,6 +620,7 @@ All major and minor version changes will be documented in this file. Details of ## [4.0.0] - 2016-11-28 - Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306) +[10.24.0]: https://github.com/gchq/CyberChef/releases/tag/v10.24.0 [10.23.0]: https://github.com/gchq/CyberChef/releases/tag/v10.23.0 [10.22.0]: https://github.com/gchq/CyberChef/releases/tag/v10.22.0 [10.21.0]: https://github.com/gchq/CyberChef/releases/tag/v10.21.0 @@ -872,6 +897,10 @@ All major and minor version changes will be documented in this file. Details of [@aby-jo]: https://github.com/aby-jo [@atsiv1]: https://github.com/atsiv1 [@fjh1997]: https://github.com/fjh1997 +[@j83305]: https://github.com/j83305 +[@BjoernAkAManf]: https://github.com/BjoernAkAManf +[@ko80240]: https://github.com/ko80240 +[@BigYellowHammer]: https://github.com/BigYellowHammer [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 @@ -1141,4 +1170,26 @@ All major and minor version changes will be documented in this file. Details of [#2194]: https://github.com/gchq/CyberChef/pull/2194 [#2193]: https://github.com/gchq/CyberChef/pull/2193 [#2192]: https://github.com/gchq/CyberChef/pull/2192 +[#2333]: https://github.com/gchq/CyberChef/pull/2333 +[#2335]: https://github.com/gchq/CyberChef/pull/2335 +[#1036]: https://github.com/gchq/CyberChef/pull/1036 +[#660]: https://github.com/gchq/CyberChef/pull/660 +[#1862]: https://github.com/gchq/CyberChef/pull/1862 +[#2322]: https://github.com/gchq/CyberChef/pull/2322 +[#2330]: https://github.com/gchq/CyberChef/pull/2330 +[#2331]: https://github.com/gchq/CyberChef/pull/2331 +[#2328]: https://github.com/gchq/CyberChef/pull/2328 +[#2326]: https://github.com/gchq/CyberChef/pull/2326 +[#2327]: https://github.com/gchq/CyberChef/pull/2327 +[#2323]: https://github.com/gchq/CyberChef/pull/2323 +[#2324]: https://github.com/gchq/CyberChef/pull/2324 +[#2321]: https://github.com/gchq/CyberChef/pull/2321 +[#2320]: https://github.com/gchq/CyberChef/pull/2320 +[#2167]: https://github.com/gchq/CyberChef/pull/2167 +[#2298]: https://github.com/gchq/CyberChef/pull/2298 +[#2317]: https://github.com/gchq/CyberChef/pull/2317 +[#2316]: https://github.com/gchq/CyberChef/pull/2316 +[#2315]: https://github.com/gchq/CyberChef/pull/2315 +[#2313]: https://github.com/gchq/CyberChef/pull/2313 +[#2311]: https://github.com/gchq/CyberChef/pull/2311 diff --git a/package-lock.json b/package-lock.json index 2ad8f7e4..66cec577 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "10.23.0", + "version": "10.24.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "10.23.0", + "version": "10.24.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 2d7b8ae8..c6412cb7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "10.23.0", + "version": "10.24.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "n1474335 ", "homepage": "https://gchq.github.io/CyberChef", From 9e5f94f5dc19cbf211018a53fc17004e7f6fb9ad Mon Sep 17 00:00:00 2001 From: Henrik Solberg Date: Mon, 27 Apr 2026 17:15:37 +0200 Subject: [PATCH 04/13] [Feature] Change to nginx-unprivileged image for better kubernetes support (#1922) Breaking change: Port number for Docker Image has changed from 80 to 8080 --- Dockerfile | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 856a16ce..ecb56814 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,7 +27,7 @@ RUN npm run build ######################################### # Package static build files into nginx # ######################################### -FROM nginx:stable-alpine AS cyberchef +FROM nginxinc/nginx-unprivileged:stable-alpine AS cyberchef LABEL maintainer="GCHQ " diff --git a/README.md b/README.md index cc18a83f..469f8f61 100755 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ docker build --tag cyberchef --ulimit nofile=10000 . ``` 2. Run the docker container ```bash -docker run -it -p 8080:80 cyberchef +docker run -it -p 8080:8080 cyberchef ``` 3. Navigate to `http://localhost:8080` in your browser @@ -45,7 +45,7 @@ docker run -it -p 8080:80 cyberchef If you prefer to skip the build process, you can use the pre-built image ```bash -docker run -it -p 8080:80 ghcr.io/gchq/cyberchef:latest +docker run -it -p 8080:8080 ghcr.io/gchq/cyberchef:latest ``` Just like before, navigate to `http://localhost:8080` in your browser. From e0ba5d28128e1dfdbe43fbed4df572fed16149e5 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:17:24 +0100 Subject: [PATCH 05/13] fix(node): enable asynchronous operation support in Node.js API (#2342) Authored-by: engin0223 Changes cherry-picked by GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Breaking change: Alters Node API - "bake" and "execute" are now declared async. --- src/node/NodeRecipe.mjs | 15 ++-- src/node/api.mjs | 6 +- tests/node/consumers/cjs-consumer.js | 4 +- tests/node/consumers/esm-consumer.mjs | 4 +- tests/node/tests/nodeApi.mjs | 113 +++++++++++++------------- 5 files changed, 70 insertions(+), 72 deletions(-) diff --git a/src/node/NodeRecipe.mjs b/src/node/NodeRecipe.mjs index bad8fc27..ade9ecb5 100644 --- a/src/node/NodeRecipe.mjs +++ b/src/node/NodeRecipe.mjs @@ -88,16 +88,17 @@ class NodeRecipe { * @param {NodeDish} dish * @returns {NodeDish} */ - execute(dish) { - return this.opList.reduce((prev, curr) => { - // CASE where opList item is op and args + async execute(dish) { + let prev = dish; + for (const curr of this.opList) { if (Object.prototype.hasOwnProperty.call(curr, "op") && Object.prototype.hasOwnProperty.call(curr, "args")) { - return curr.op(prev, curr.args); + prev = await curr.op(prev, curr.args); + } else { + prev = await curr(prev); } - // CASE opList item is just op. - return curr(prev); - }, dish); + } + return prev; } } diff --git a/src/node/api.mjs b/src/node/api.mjs index 88b3f834..a02af9d3 100644 --- a/src/node/api.mjs +++ b/src/node/api.mjs @@ -324,10 +324,10 @@ export function help(input) { * @returns {NodeDish} of the result * @throws {TypeError} if invalid recipe given. */ -export function bake(input, recipeConfig) { - const recipe = new NodeRecipe(recipeConfig); +export async function bake(input, recipeConfig) { + const recipe = new NodeRecipe(recipeConfig); const dish = ensureIsDish(input); - return recipe.execute(dish); + return await recipe.execute(dish); } diff --git a/tests/node/consumers/cjs-consumer.js b/tests/node/consumers/cjs-consumer.js index 3a759481..f13d9c35 100644 --- a/tests/node/consumers/cjs-consumer.js +++ b/tests/node/consumers/cjs-consumer.js @@ -8,9 +8,9 @@ const assert = require("assert"); -require("cyberchef").then(chef => { +require("cyberchef").then(async chef => { - const d = chef.bake("Testing, 1 2 3", [ + const d = await chef.bake("Testing, 1 2 3", [ chef.toHex, chef.reverse, { diff --git a/tests/node/consumers/esm-consumer.mjs b/tests/node/consumers/esm-consumer.mjs index 2919e533..3a2648e1 100644 --- a/tests/node/consumers/esm-consumer.mjs +++ b/tests/node/consumers/esm-consumer.mjs @@ -9,7 +9,7 @@ import assert from "assert"; import chef from "cyberchef"; import { bake, toHex, reverse, unique, multiply } from "cyberchef"; -const a = bake("Testing, 1 2 3", [ +const a = await bake("Testing, 1 2 3", [ toHex, reverse, { @@ -28,7 +28,7 @@ const a = bake("Testing, 1 2 3", [ assert.equal(a.value, "630957449041920"); -const b = chef.bake("Testing, 1 2 3", [ +const b = await chef.bake("Testing, 1 2 3", [ chef.toHex, chef.reverse, { diff --git a/tests/node/tests/nodeApi.mjs b/tests/node/tests/nodeApi.mjs index 92d4d991..2510ef17 100644 --- a/tests/node/tests/nodeApi.mjs +++ b/tests/node/tests/nodeApi.mjs @@ -170,77 +170,77 @@ TestRegister.addApiTests([ assert(chef.bake); }), - it("chef.bake: should return NodeDish", () => { - const result = chef.bake("input", "to base 64"); + it("chef.bake: should return NodeDish", async () => { + const result = await chef.bake("input", "to base 64"); assert(result instanceof NodeDish); }), - it("chef.bake: should take an input and an op name and perform it", () => { - const result = chef.bake("some input", "to base 32"); + it("chef.bake: should take an input and an op name and perform it", async () => { + const result = await chef.bake("some input", "to base 32"); assert.strictEqual(result.toString(), "ONXW2ZJANFXHA5LU"); }), - it("chef.bake: should complain if recipe isnt a valid object", () => { - assert.throws(() => chef.bake("some input", 3264), { + it("chef.bake: should complain if recipe isnt a valid object", async () => { + await assert.rejects(() => chef.bake("some input", 3264), { name: "TypeError", message: "Recipe can only contain function names or functions" }); }), - it("chef.bake: Should complain if string op is invalid", () => { - assert.throws(() => chef.bake("some input", "not a valid operation"), { + it("chef.bake: Should complain if string op is invalid", async () => { + await assert.rejects(() => chef.bake("some input", "not a valid operation"), { name: "TypeError", message: "Couldn't find an operation with name 'not a valid operation'." }); }), - it("chef.bake: Should take an input and an operation and perform it", () => { - const result = chef.bake("https://google.com/search?q=help", chef.parseURI); + it("chef.bake: Should take an input and an operation and perform it", async () => { + const result = await chef.bake("https://google.com/search?q=help", chef.parseURI); assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = help\n"); }), - it("chef.bake: Should complain if an invalid operation is inputted", () => { - assert.throws(() => chef.bake("https://google.com/search?q=help", () => {}), { + it("chef.bake: Should complain if an invalid operation is inputted", async () => { + await assert.rejects(() => chef.bake("https://google.com/search?q=help", () => {}), { name: "TypeError", message: "Inputted function not a Chef operation." }); }), - it("chef.bake: accepts an array of operation names and performs them all in order", () => { - const result = chef.bake("https://google.com/search?q=that's a complicated question", ["URL encode", "URL decode", "Parse URI"]); + it("chef.bake: accepts an array of operation names and performs them all in order", async () => { + const result = await chef.bake("https://google.com/search?q=that's a complicated question", ["URL encode", "URL decode", "Parse URI"]); assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = that's a complicated question\n"); }), - it("chef.bake: forgiving with operation names", () =>{ - const result = chef.bake("https://google.com/search?q=that's a complicated question", ["urlencode", "url decode", "parseURI"]); + it("chef.bake: forgiving with operation names", async () =>{ + const result = await chef.bake("https://google.com/search?q=that's a complicated question", ["urlencode", "url decode", "parseURI"]); assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = that's a complicated question\n"); }), - it("chef.bake: forgiving with operation names", () =>{ - const result = chef.bake("hello", ["to base 64"]); + it("chef.bake: forgiving with operation names", async () =>{ + const result = await chef.bake("hello", ["to base 64"]); assert.strictEqual(result.toString(), "aGVsbG8="); }), - it("chef.bake: if recipe is empty array, return input as dish", () => { - const result = chef.bake("some input", []); + it("chef.bake: if recipe is empty array, return input as dish", async () => { + const result = await chef.bake("some input", []); assert.strictEqual(result.toString(), "some input"); assert(result instanceof NodeDish, "Result is not instance of NodeDish"); }), - it("chef.bake: accepts an array of operations as recipe", () => { - const result = chef.bake("https://google.com/search?q=that's a complicated question", [chef.URLEncode, chef.URLDecode, chef.parseURI]); + it("chef.bake: accepts an array of operations as recipe", async () => { + const result = await chef.bake("https://google.com/search?q=that's a complicated question", [chef.URLEncode, chef.URLDecode, chef.parseURI]); assert.strictEqual(result.toString(), "Protocol:\thttps:\nHostname:\tgoogle.com\nPath name:\t/search\nArguments:\n\tq = that's a complicated question\n"); }), - it("should complain if an invalid operation is inputted as part of array", () => { - assert.throws(() => chef.bake("something", [() => {}]), { + it("should complain if an invalid operation is inputted as part of array", async () => { + await assert.rejects(() => chef.bake("something", [() => {}]), { name: "TypeError", message: "Inputted function not a Chef operation." }); }), - it("chef.bake: should take single JSON object describing op and args OBJ", () => { - const result = chef.bake("some input", { + it("chef.bake: should take single JSON object describing op and args OBJ", async () => { + const result = await chef.bake("some input", { op: chef.toHex, args: { Delimiter: "Colon" @@ -249,23 +249,23 @@ TestRegister.addApiTests([ assert.strictEqual(result.toString(), "73:6f:6d:65:20:69:6e:70:75:74"); }), - it("chef.bake: should take single JSON object desribing op with optional args", () => { - const result = chef.bake("some input", { + it("chef.bake: should take single JSON object desribing op with optional args", async () => { + const result = await chef.bake("some input", { op: chef.toHex, }); assert.strictEqual(result.toString(), "73 6f 6d 65 20 69 6e 70 75 74"); }), - it("chef.bake: should take single JSON object describing op and args ARRAY", () => { - const result = chef.bake("some input", { + it("chef.bake: should take single JSON object describing op and args ARRAY", async () => { + const result = await chef.bake("some input", { op: chef.toHex, args: ["Colon"] }); assert.strictEqual(result.toString(), "73:6f:6d:65:20:69:6e:70:75:74"); }), - it("chef.bake: should error if op in JSON is not chef op", () => { - assert.throws(() => chef.bake("some input", { + it("chef.bake: should error if op in JSON is not chef op", async () => { + await assert.rejects(() => chef.bake("some input", { op: () => {}, args: ["Colon"], }), { @@ -274,8 +274,8 @@ TestRegister.addApiTests([ }); }), - it("chef.bake: should take multiple ops in JSON object form, some ops by string", () => { - const result = chef.bake("some input", [ + it("chef.bake: should take multiple ops in JSON object form, some ops by string", async () => { + const result = await chef.bake("some input", [ { op: chef.toHex, args: ["Colon"] @@ -290,8 +290,8 @@ TestRegister.addApiTests([ assert.strictEqual(result.toString(), "67;63;72;66;146;72;66;144;72;66;65;72;62;60;72;66;71;72;66;145;72;67;60;72;67;65;72;67;64"); }), - it("chef.bake: should take multiple ops in JSON object form, some without args", () => { - const result = chef.bake("some input", [ + it("chef.bake: should take multiple ops in JSON object form, some without args", async () => { + const result = await chef.bake("some input", [ { op: chef.toHex, }, @@ -305,8 +305,8 @@ TestRegister.addApiTests([ assert.strictEqual(result.toString(), "67;63;40;66;146;40;66;144;40;66;65;40;62;60;40;66;71;40;66;145;40;67;60;40;67;65;40;67;64"); }), - it("chef.bake: should handle op with multiple args", () => { - const result = chef.bake("some input", { + it("chef.bake: should handle op with multiple args", async () => { + const result = await chef.bake("some input", { op: "to morse code", args: { formatOptions: "Dash/Dot", @@ -317,13 +317,13 @@ TestRegister.addApiTests([ assert.strictEqual(result.toString(), "DotDotDot\\DashDashDash\\DashDash\\Dot,DotDot\\DashDot\\DotDashDashDot\\DotDotDash\\Dash"); }), - it("chef.bake: should take compact JSON format from Chef Website as recipe", () => { - const result = chef.bake("some input", [{"op": "To Morse Code", "args": ["Dash/Dot", "Backslash", "Comma"]}, {"op": "Hex to PEM", "args": ["SOMETHING"]}, {"op": "To Snake case", "args": [false]}]); + it("chef.bake: should take compact JSON format from Chef Website as recipe", async () => { + const result = await chef.bake("some input", [{"op": "To Morse Code", "args": ["Dash/Dot", "Backslash", "Comma"]}, {"op": "Hex to PEM", "args": ["SOMETHING"]}, {"op": "To Snake case", "args": [false]}]); assert.strictEqual(result.toString(), "begin_something_anananaaaaak_da_aaak_da_aaaaananaaaaaaan_da_aaaaaaanan_da_aaak_end_something"); }), - it("chef.bake: should accept Clean JSON format from Chef website as recipe", () => { - const result = chef.bake("some input", [ + it("chef.bake: should accept Clean JSON format from Chef website as recipe", async () => { + const result = await chef.bake("some input", [ { "op": "To Morse Code", "args": ["Dash/Dot", "Backslash", "Comma"] }, { "op": "Hex to PEM", @@ -334,8 +334,8 @@ TestRegister.addApiTests([ assert.strictEqual(result.toString(), "begin_something_anananaaaaak_da_aaak_da_aaaaananaaaaaaan_da_aaaaaaanan_da_aaak_end_something"); }), - it("chef.bake: should accept Clean JSON format from Chef website - args optional", () => { - const result = chef.bake("some input", [ + it("chef.bake: should accept Clean JSON format from Chef website - args optional", async () => { + const result = await chef.bake("some input", [ { "op": "To Morse Code" }, { "op": "Hex to PEM", "args": ["SOMETHING"] }, @@ -345,31 +345,28 @@ TestRegister.addApiTests([ assert.strictEqual(result.toString(), "begin_something_aaaaaaaaaaaaaa_end_something"); }), - it("chef.bake: should accept operation names from Chef Website which contain forward slash", () => { - const result = chef.bake("I'll have the test salmon", [ + it("chef.bake: should accept operation names from Chef Website which contain forward slash", async () => { + const result = await chef.bake("I'll have the test salmon", [ { "op": "Find / Replace", "args": [{ "option": "Regex", "string": "test" }, "good", true, false, true, false]} ]); assert.strictEqual(result.toString(), "I'll have the good salmon"); }), - it("chef.bake: should accept operation names from Chef Website which contain a hyphen", () => { - const result = chef.bake("I'll have the test salmon", [ + it("chef.bake: should accept operation names from Chef Website which contain a hyphen", async () => { + const result = await chef.bake("I'll have the test salmon", [ { "op": "Adler-32 Checksum", "args": [] } ]); assert.strictEqual(result.toString(), "6e4208f8"); }), - it("chef.bake: should accept operation names from Chef Website which contain a period", () => { - const result = chef.bake("30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f", [ + it("chef.bake: should accept operation names from Chef Website which contain a period", async () => { + const result = await chef.bake("30 13 02 01 05 16 0e 41 6e 79 62 6f 64 79 20 74 68 65 72 65 3f", [ { "op": "Parse ASN.1 hex string", "args": [0, 32] } ]); - assert.strictEqual(result.toString(), `SEQUENCE - INTEGER 05 - IA5String 'Anybody there?' -`); + assert.strictEqual(result.toString(), `SEQUENCE\n INTEGER 05\n IA5String 'Anybody there?'\n`); }), it("Excluded operations: throw a sensible error when you try and call one", () => { @@ -381,16 +378,16 @@ TestRegister.addApiTests([ } }), - it("chef.bake: cannot accept flowControl operations in recipe", () => { - assert.throws(() => chef.bake("some input", "magic"), { + it("chef.bake: cannot accept flowControl operations in recipe", async () => { + await assert.rejects(() => chef.bake("some input", "magic"), { name: "TypeError", message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API" }); - assert.throws(() => chef.bake("some input", magic), { + await assert.rejects(() => chef.bake("some input", magic), { name: "TypeError", message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API" }); - assert.throws(() => chef.bake("some input", ["to base 64", "magic"]), { + await assert.rejects(() => chef.bake("some input", ["to base 64", "magic"]), { name: "TypeError", message: "flowControl operations like Magic are not currently allowed in recipes for chef.bake in the Node API" }); From 864afa85aaf736514de79dcd751bfac5b8ec05f0 Mon Sep 17 00:00:00 2001 From: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> Date: Tue, 28 Apr 2026 07:18:44 +0100 Subject: [PATCH 06/13] Make compatible with node >=22 (#2273) Breaking change: uplifts the minimum version of node required to v20 --- .devcontainer/devcontainer.json | 2 +- .github/workflows/master.yml | 2 +- .github/workflows/pull_requests.yml | 2 +- .github/workflows/releases.yml | 2 +- .nvmrc | 2 +- Dockerfile | 2 +- README.md | 2 +- babel.config.js | 5 ++++- package.json | 2 +- src/core/ChefWorker.js | 2 +- src/core/Recipe.mjs | 2 +- src/core/lib/Magic.mjs | 2 +- src/node/api.mjs | 2 +- src/web/index.js | 4 ++-- src/web/static/sitemap.mjs | 2 +- tests/node/tests/Categories.mjs | 4 ++-- 16 files changed, 21 insertions(+), 18 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 92ebd43c..9b493bb6 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -3,7 +3,7 @@ { "name": "CyberChef", // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile - "image": "mcr.microsoft.com/devcontainers/javascript-node:1-18-bookworm", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22-trixie", // Features to add to the dev container. More info: https://containers.dev/features. "features": { diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 62f19745..78ec1f41 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -21,7 +21,7 @@ jobs: - name: Set node version uses: actions/setup-node@v6 with: - node-version: 18 + node-version: '20.x' registry-url: "https://registry.npmjs.org" - name: Install diff --git a/.github/workflows/pull_requests.yml b/.github/workflows/pull_requests.yml index efa6f6c4..7c731176 100644 --- a/.github/workflows/pull_requests.yml +++ b/.github/workflows/pull_requests.yml @@ -17,7 +17,7 @@ jobs: - name: Set node version uses: actions/setup-node@v6 with: - node-version: 18 + node-version: '20.x' registry-url: "https://registry.npmjs.org" - name: Install diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 67e01d02..8230dbfd 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -27,7 +27,7 @@ jobs: - name: Set node version uses: actions/setup-node@v6 with: - node-version: 18 + node-version: '20.x' registry-url: "https://registry.npmjs.org" - name: Install diff --git a/.nvmrc b/.nvmrc index 3c032078..2bd5a0a9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +22 diff --git a/Dockerfile b/Dockerfile index ecb56814..b0a1024d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # Modifier --platform=$BUILDPLATFORM limits the platform to "BUILDPLATFORM" during buildx multi-platform builds # This is because npm "chromedriver" package is not compatiable with all platforms # For more info see: https://docs.docker.com/build/building/multi-platform/#cross-compilation -FROM --platform=$BUILDPLATFORM node:18-alpine AS builder +FROM --platform=$BUILDPLATFORM node:22-alpine AS builder WORKDIR /app diff --git a/README.md b/README.md index 469f8f61..c1c90f2e 100755 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ CyberChef is built to support ## Node.js support -CyberChef is built to fully support Node.js `v16`. For more information, see the ["Node API" wiki page](https://github.com/gchq/CyberChef/wiki/Node-API) +CyberChef is built to fully support Node.js `v22`. For more information, see the ["Node API" wiki page](https://github.com/gchq/CyberChef/wiki/Node-API) ## Contributing diff --git a/babel.config.js b/babel.config.js index 178271fb..b9efd549 100644 --- a/babel.config.js +++ b/babel.config.js @@ -16,6 +16,9 @@ module.exports = function(api) { "regenerator": true } ] - ] + ], + "generatorOpts": { + "importAttributesKeyword": "with" + } }; }; diff --git a/package.json b/package.json index c6412cb7..a6dd4979 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "browserslist": [ "Chrome >= 50", "Firefox >= 38", - "node >= 16" + "node >= 22" ], "devDependencies": { "@babel/eslint-parser": "^7.28.6", diff --git a/src/core/ChefWorker.js b/src/core/ChefWorker.js index a43993f9..43f595f3 100644 --- a/src/core/ChefWorker.js +++ b/src/core/ChefWorker.js @@ -7,7 +7,7 @@ */ import Chef from "./Chef.mjs"; -import OperationConfig from "./config/OperationConfig.json" assert {type: "json"}; +import OperationConfig from "./config/OperationConfig.json" with { type: "json" }; import OpModules from "./config/modules/OpModules.mjs"; import loglevelMessagePrefix from "loglevel-message-prefix"; diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs index b4a10e03..84c91d61 100755 --- a/src/core/Recipe.mjs +++ b/src/core/Recipe.mjs @@ -4,7 +4,7 @@ * @license Apache-2.0 */ -import OperationConfig from "./config/OperationConfig.json" assert {type: "json"}; +import OperationConfig from "./config/OperationConfig.json" with { type: "json" }; import OperationError from "./errors/OperationError.mjs"; import Operation from "./Operation.mjs"; import DishError from "./errors/DishError.mjs"; diff --git a/src/core/lib/Magic.mjs b/src/core/lib/Magic.mjs index 14111ec7..ad407de6 100644 --- a/src/core/lib/Magic.mjs +++ b/src/core/lib/Magic.mjs @@ -1,4 +1,4 @@ -import OperationConfig from "../config/OperationConfig.json" assert {type: "json"}; +import OperationConfig from "../config/OperationConfig.json" with { type: "json" }; import Utils, { isWorkerEnvironment } from "../Utils.mjs"; import Recipe from "../Recipe.mjs"; import Dish from "../Dish.mjs"; diff --git a/src/node/api.mjs b/src/node/api.mjs index a02af9d3..8002a8ac 100644 --- a/src/node/api.mjs +++ b/src/node/api.mjs @@ -10,7 +10,7 @@ import NodeDish from "./NodeDish.mjs"; import NodeRecipe from "./NodeRecipe.mjs"; -import OperationConfig from "../core/config/OperationConfig.json" assert {type: "json"}; +import OperationConfig from "../core/config/OperationConfig.json" with { type: "json" }; import { sanitise, removeSubheadingsFromArray, sentenceToCamelCase } from "./apiUtils.mjs"; import ExcludedOperationError from "../core/errors/ExcludedOperationError.mjs"; diff --git a/src/web/index.js b/src/web/index.js index 90142b34..110f0d2b 100755 --- a/src/web/index.js +++ b/src/web/index.js @@ -17,8 +17,8 @@ import * as CanvasComponents from "../core/lib/CanvasComponents.mjs"; // CyberChef import App from "./App.mjs"; -import Categories from "../core/config/Categories.json" assert {type: "json"}; -import OperationConfig from "../core/config/OperationConfig.json" assert {type: "json"}; +import Categories from "../core/config/Categories.json" with { type: "json" }; +import OperationConfig from "../core/config/OperationConfig.json" with { type: "json" }; /** diff --git a/src/web/static/sitemap.mjs b/src/web/static/sitemap.mjs index 4f8101d4..f373a277 100644 --- a/src/web/static/sitemap.mjs +++ b/src/web/static/sitemap.mjs @@ -1,5 +1,5 @@ import sm from "sitemap"; -import OperationConfig from "../../core/config/OperationConfig.json" assert { type: "json" }; +import OperationConfig from "../../core/config/OperationConfig.json" with { type: "json" }; /** * Generates an XML sitemap for all CyberChef operations and a number of recipes. diff --git a/tests/node/tests/Categories.mjs b/tests/node/tests/Categories.mjs index e6f8bd72..070d78d7 100644 --- a/tests/node/tests/Categories.mjs +++ b/tests/node/tests/Categories.mjs @@ -1,6 +1,6 @@ import TestRegister from "../../lib/TestRegister.mjs"; -import Categories from "../../../src/core/config/Categories.json" assert {type: "json"}; -import OperationConfig from "../../../src/core/config/OperationConfig.json" assert {type: "json"}; +import Categories from "../../../src/core/config/Categories.json" with { type: "json" }; +import OperationConfig from "../../../src/core/config/OperationConfig.json" with { type: "json" }; import it from "../assertionHandler.mjs"; import assert from "assert"; From 9641ae07f92e9af50f10e978385465b2f4a36c4d Mon Sep 17 00:00:00 2001 From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com> Date: Tue, 28 Apr 2026 10:02:00 +0100 Subject: [PATCH 07/13] Fix XSS in Show Base64 offsets (#2346) --- src/core/operations/ShowBase64Offsets.mjs | 56 +++++++++++------------ tests/operations/tests/Base64.mjs | 11 +++++ 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/src/core/operations/ShowBase64Offsets.mjs b/src/core/operations/ShowBase64Offsets.mjs index 37d8a6ce..a375d7db 100644 --- a/src/core/operations/ShowBase64Offsets.mjs +++ b/src/core/operations/ShowBase64Offsets.mjs @@ -77,84 +77,84 @@ class ShowBase64Offsets extends Operation { staticSection = offset0.slice(0, -3); offset0 = "" + - staticSection + "" + - "" + offset0.substr(offset0.length - 3, 1) + "" + - "" + offset0.substr(offset0.length - 2) + ""; + Utils.escapeHtml(staticSection) + "" + + "" + Utils.escapeHtml(offset0.substr(offset0.length - 3, 1)) + "" + + "" + Utils.escapeHtml(offset0.substr(offset0.length - 2)) + ""; } else if (len0 % 4 === 3) { staticSection = offset0.slice(0, -2); offset0 = "" + - staticSection + "" + - "" + offset0.substr(offset0.length - 2, 1) + "" + - "" + offset0.substr(offset0.length - 1) + ""; + Utils.escapeHtml(staticSection) + "" + + "" + Utils.escapeHtml(offset0.substr(offset0.length - 2, 1)) + "" + + "" + Utils.escapeHtml(offset0.substr(offset0.length - 1)) + ""; } else { staticSection = offset0; offset0 = "" + - staticSection + ""; + Utils.escapeHtml(staticSection) + ""; } if (!showVariable) { - offset0 = staticSection; + offset0 = Utils.escapeHtml(staticSection); } // Highlight offset 1 - padding = "" + offset1.substr(0, 1) + "" + - "" + offset1.substr(1, 1) + ""; + padding = "" + Utils.escapeHtml(offset1.substr(0, 1)) + "" + + "" + Utils.escapeHtml(offset1.substr(1, 1)) + ""; offset1 = offset1.substr(2); if (len1 % 4 === 2) { staticSection = offset1.slice(0, -3); offset1 = padding + "" + - staticSection + "" + - "" + offset1.substr(offset1.length - 3, 1) + "" + - "" + offset1.substr(offset1.length - 2) + ""; + Utils.escapeHtml(staticSection) + "" + + "" + Utils.escapeHtml(offset1.substr(offset1.length - 3, 1)) + "" + + "" + Utils.escapeHtml(offset1.substr(offset1.length - 2)) + ""; } else if (len1 % 4 === 3) { staticSection = offset1.slice(0, -2); offset1 = padding + "" + - staticSection + "" + - "" + offset1.substr(offset1.length - 2, 1) + "" + - "" + offset1.substr(offset1.length - 1) + ""; + Utils.escapeHtml(staticSection) + "" + + "" + Utils.escapeHtml(offset1.substr(offset1.length - 2, 1)) + "" + + "" + Utils.escapeHtml(offset1.substr(offset1.length - 1)) + ""; } else { staticSection = offset1; offset1 = padding + "" + - staticSection + ""; + Utils.escapeHtml(staticSection) + ""; } if (!showVariable) { - offset1 = staticSection; + offset1 = Utils.escapeHtml(staticSection); } // Highlight offset 2 - padding = "" + offset2.substr(0, 2) + "" + - "" + offset2.substr(2, 1) + ""; + padding = "" + Utils.escapeHtml(offset2.substr(0, 2)) + "" + + "" + Utils.escapeHtml(offset2.substr(2, 1)) + ""; offset2 = offset2.substr(3); if (len2 % 4 === 2) { staticSection = offset2.slice(0, -3); offset2 = padding + "" + - staticSection + "" + - "" + offset2.substr(offset2.length - 3, 1) + "" + - "" + offset2.substr(offset2.length - 2) + ""; + Utils.escapeHtml(staticSection) + "" + + "" + Utils.escapeHtml(offset2.substr(offset2.length - 3, 1)) + "" + + "" + Utils.escapeHtml(offset2.substr(offset2.length - 2)) + ""; } else if (len2 % 4 === 3) { staticSection = offset2.slice(0, -2); offset2 = padding + "" + - staticSection + "" + - "" + offset2.substr(offset2.length - 2, 1) + "" + - "" + offset2.substr(offset2.length - 1) + ""; + Utils.escapeHtml(staticSection) + "" + + "" + Utils.escapeHtml(offset2.substr(offset2.length - 2, 1)) + "" + + "" + Utils.escapeHtml(offset2.substr(offset2.length - 1)) + ""; } else { staticSection = offset2; offset2 = padding + "" + - staticSection + ""; + Utils.escapeHtml(staticSection) + ""; } if (!showVariable) { - offset2 = staticSection; + offset2 = Utils.escapeHtml(staticSection); } return (showVariable ? "Characters highlighted in green could change if the input is surrounded by more data." + diff --git a/tests/operations/tests/Base64.mjs b/tests/operations/tests/Base64.mjs index 6e6fa703..5dca403a 100644 --- a/tests/operations/tests/Base64.mjs +++ b/tests/operations/tests/Base64.mjs @@ -116,4 +116,15 @@ TestRegister.addTests([ }, ], }, + { + name: "Show Base64 offsets: escapes static output", + input: "\x00\x10\x83\x10\x51\x87", + expectedOutput: "<script>\n<AQmsBRk66\n<ia1AEIM6", + recipeConfig: [ + { + op: "Show Base64 offsets", + args: ["