From 9e5f94f5dc19cbf211018a53fc17004e7f6fb9ad Mon Sep 17 00:00:00 2001 From: Henrik Solberg Date: Mon, 27 Apr 2026 17:15:37 +0200 Subject: [PATCH 1/8] [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 2/8] 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 3/8] 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 4/8] 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: ["