From 9e5f94f5dc19cbf211018a53fc17004e7f6fb9ad Mon Sep 17 00:00:00 2001
From: Henrik Solberg
Date: Mon, 27 Apr 2026 17:15:37 +0200
Subject: [PATCH 01/53] [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 02/53] 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 03/53] 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 04/53] 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: ["
Date: Mon, 18 May 2026 15:51:56 +0100
Subject: [PATCH 33/53] Bump nginxinc/nginx-unprivileged from `808f784` to
`b9f7ba1` (#2389)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Dockerfile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Dockerfile b/Dockerfile
index c8d40424..873ca32b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -27,7 +27,7 @@ RUN npm run build
#########################################
# Package static build files into nginx #
#########################################
-FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:808f7846d21a9c94cf53833e8807a00a33fd0b65cc47fb05b79efe366c2d201f AS cyberchef
+FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:b9f7ba14f1f7bd3d40d7753584048f92c3aef9ccf5fab14efe4451a8d4c04d63 AS cyberchef
LABEL maintainer="GCHQ "
From 2cf778c25390d1015a9de5abd2f6a33875b1008b Mon Sep 17 00:00:00 2001
From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com>
Date: Wed, 20 May 2026 15:32:46 +0100
Subject: [PATCH 34/53] Include git ref in website download zip name (#2339)
---
Gruntfile.js | 12 ++++++++----
src/web/html/index.html | 6 +++---
2 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/Gruntfile.js b/Gruntfile.js
index 6f87b2a6..475701b8 100755
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -89,6 +89,8 @@ module.exports = function (grunt) {
const compileYear = grunt.template.today("UTC:yyyy"),
compileTime = grunt.template.today("UTC:dd/mm/yyyy HH:MM:ss") + " UTC",
pkg = grunt.file.readJSON("package.json"),
+ version = process.env.GITHUB_SHA || `v${pkg.version}`,
+ downloadZipFilename = `CyberChef_${version}.zip`,
webpackConfig = require("./webpack.config.js"),
BUILD_CONSTANTS = {
COMPILE_YEAR: JSON.stringify(compileYear),
@@ -129,7 +131,9 @@ module.exports = function (grunt) {
chunks: ["main"],
compileYear: compileYear,
compileTime: compileTime,
- version: pkg.version,
+ version: version,
+ latestReleaseVersion: pkg.version,
+ downloadZipFilename: downloadZipFilename,
minify: {
removeComments: true,
collapseWhitespace: true,
@@ -245,7 +249,7 @@ module.exports = function (grunt) {
"!build/prod/index.html",
"!build/prod/BundleAnalyzerReport.html",
],
- dest: `build/prod/CyberChef_v${pkg.version}.zip`
+ dest: `build/prod/${downloadZipFilename}`
}
},
connect: {
@@ -333,12 +337,12 @@ module.exports = function (grunt) {
switch (process.platform) {
case "darwin":
return chainCommands([
- `shasum -a 256 build/prod/CyberChef_v${pkg.version}.zip | awk '{print $1;}' > build/prod/sha256digest.txt`,
+ `shasum -a 256 build/prod/${downloadZipFilename} | awk '{print $1;}' > build/prod/sha256digest.txt`,
`sed -i '' -e "s/DOWNLOAD_HASH_PLACEHOLDER/$(cat build/prod/sha256digest.txt)/" build/prod/index.html`
]);
default:
return chainCommands([
- `sha256sum build/prod/CyberChef_v${pkg.version}.zip | awk '{print $1;}' > build/prod/sha256digest.txt`,
+ `sha256sum build/prod/${downloadZipFilename} | awk '{print $1;}' > build/prod/sha256digest.txt`,
`sed -i -e "s/DOWNLOAD_HASH_PLACEHOLDER/$(cat build/prod/sha256digest.txt)/" build/prod/index.html`
]);
}
diff --git a/src/web/html/index.html b/src/web/html/index.html
index 335f8c44..6ce8dd9a 100755
--- a/src/web/html/index.html
+++ b/src/web/html/index.html
@@ -868,15 +868,15 @@
Be aware that the standalone version will never update itself, meaning it will not receive bug fixes or new features until you re-download newer versions manually.
- CyberChef v<%= htmlWebpackPlugin.options.version %>
+ CyberChef <%= htmlWebpackPlugin.options.version %>
- Build time: <%= htmlWebpackPlugin.options.compileTime %>
- - The changelog for this version can be viewed here
+ - The changelog for this version can be viewed here
- © Crown Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %>
- Released under the Apache Licence, Version 2.0
- SHA256 hash: DOWNLOAD_HASH_PLACEHOLDER
- Download ZIP file
+ Download ZIP file