From cc38f3a53f6e7876540e02802610553b723e1052 Mon Sep 17 00:00:00 2001 From: Subhadeep Date: Mon, 15 Jun 2026 18:07:04 +0530 Subject: [PATCH 01/81] Fix: Add input validation for XOR Checksum blocksize (#2537) (#2542) --- src/core/operations/XORChecksum.mjs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/core/operations/XORChecksum.mjs b/src/core/operations/XORChecksum.mjs index 1603a265..ca9c6fac 100644 --- a/src/core/operations/XORChecksum.mjs +++ b/src/core/operations/XORChecksum.mjs @@ -7,12 +7,12 @@ import Operation from "../Operation.mjs"; import Utils from "../Utils.mjs"; import { toHex } from "../lib/Hex.mjs"; +import OperationError from "../errors/OperationError.mjs"; /** * XOR Checksum operation */ class XORChecksum extends Operation { - /** * XORChecksum constructor */ @@ -21,7 +21,8 @@ class XORChecksum extends Operation { this.name = "XOR Checksum"; this.module = "Crypto"; - this.description = "XOR Checksum splits the input into blocks of a configurable size and performs the XOR operation on these blocks."; + this.description = + "XOR Checksum splits the input into blocks of a configurable size and performs the XOR operation on these blocks."; this.infoURL = "https://wikipedia.org/wiki/XOR"; this.inputType = "ArrayBuffer"; this.outputType = "string"; @@ -29,7 +30,7 @@ class XORChecksum extends Operation { { name: "Blocksize", type: "number", - value: 4 + value: 4, }, ]; } @@ -41,6 +42,12 @@ class XORChecksum extends Operation { */ run(input, args) { const blocksize = args[0]; + + + if (!Number.isInteger(blocksize) || blocksize <= 0) { + throw new OperationError("Blocksize must be a positive integer."); + } + input = new Uint8Array(input); const res = Array(blocksize); From 50a7319b69ff6fd4473c8648fcaac1eeb3027dac Mon Sep 17 00:00:00 2001 From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:31:55 +0100 Subject: [PATCH 02/81] Update website references (#2566) --- README.md | 13 +++++++------ SECURITY.md | 4 +++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 50b1a44d..f74b78a5 100755 --- a/README.md +++ b/README.md @@ -12,13 +12,9 @@ CyberChef is a simple, intuitive web app for carrying out all manner of "cyber" The tool is designed to enable both technical and non-technical analysts to manipulate data in complex ways without having to deal with complex tools or algorithms. It was conceived, designed, built and incrementally improved by an analyst in their 10% innovation time over several years. -## Live demo +## Official website -CyberChef is still under active development. As a result, it shouldn't be considered a finished product. There is still testing and bug fixing to do, new features to be added and additional documentation to write. Please contribute! - -Cryptographic operations in CyberChef should not be relied upon to provide security in any situation. No guarantee is offered for their correctness. - -[A live demo can be found here][1] - have fun! +[CyberChef's official website can be found here][1] - have fun! ## Running Locally with Docker @@ -124,6 +120,11 @@ CyberChef is built to support CyberChef is built to fully support Node.js `v24`. For more information, see the ["Node API" wiki page](https://github.com/gchq/CyberChef/wiki/Node-API) +## Security + +Please see the [CyberChef security policy](./SECURITY.md). + + ## Contributing Contributing a new operation to CyberChef is super easy! The quickstart script will walk you through the process. If you can write basic JavaScript, you can write a CyberChef operation. diff --git a/SECURITY.md b/SECURITY.md index 92382460..90cdd750 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,11 +1,13 @@ # Security Policy -## Supported Versions +## Support CyberChef is supported on a best endeavours basis. Patches will be applied to the latest version rather than retroactively to older versions. To ensure you are using the most secure version of CyberChef, please make sure you have the [latest release](https://github.com/gchq/CyberChef/releases/latest). [The official website](https://gchq.github.io/CyberChef/) is always up to date. +No guarantee is offered for the correctness or security of CyberChef. In paticular, the security of cryptographic operations should not be relied upon. + ## Reporting a Vulnerability If you discover a vulnerability in CyberChef, please do not publicly disclose it, and do not create a GitHub issue. From 85db3be5d0096859b810f0e8d3e151d5dc9b948f Mon Sep 17 00:00:00 2001 From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:41:55 +0100 Subject: [PATCH 03/81] Chart operation prototype protection (#2569) --- src/core/lib/Charts.mjs | 6 +- src/core/lib/Protocol.mjs | 14 +-- tests/node/index.mjs | 1 + .../lib/ChartsProtocolPrototypePollution.mjs | 90 +++++++++++++++++++ 4 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 tests/node/tests/lib/ChartsProtocolPrototypePollution.mjs diff --git a/src/core/lib/Charts.mjs b/src/core/lib/Charts.mjs index 6cb63f60..a70a900f 100644 --- a/src/core/lib/Charts.mjs +++ b/src/core/lib/Charts.mjs @@ -153,7 +153,7 @@ export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHe ); let xValues = new Set(); - const series = {}; + const series = Object.create(null); values.forEach(row => { const serie = row[0], @@ -163,14 +163,14 @@ export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHe if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10."); xValues.add(xVal); - if (typeof series[serie] === "undefined") series[serie] = {}; + if (typeof series[serie] === "undefined") series[serie] = Object.create(null); series[serie][xVal] = val; }); xValues = new Array(...xValues); const seriesList = []; - for (const seriesName in series) { + for (const seriesName of Object.keys(series)) { const serie = series[seriesName]; seriesList.push({name: seriesName, data: serie}); } diff --git a/src/core/lib/Protocol.mjs b/src/core/lib/Protocol.mjs index dfb8b197..1875e40c 100644 --- a/src/core/lib/Protocol.mjs +++ b/src/core/lib/Protocol.mjs @@ -8,6 +8,7 @@ import BigNumber from "bignumber.js"; import {toHexFast} from "../lib/Hex.mjs"; +import Utils from "../Utils.mjs"; /** * Recursively displays a JSON object as an HTML table @@ -25,15 +26,16 @@ export function objToTable(obj, nested=false) { Value `; - for (const key in obj) { - if (typeof obj[key] === "function") + for (const key of Object.keys(obj)) { + const value = obj[key]; + if (typeof value === "function") continue; - html += `${key}`; - if (typeof obj[key] === "object") - html += `${objToTable(obj[key], true)}`; + html += `${Utils.escapeHtml(String(key))}`; + if (value !== null && typeof value === "object") + html += `${objToTable(value, true)}`; else - html += `${obj[key]}`; + html += `${Utils.escapeHtml(String(value))}`; html += ""; } html += ""; diff --git a/tests/node/index.mjs b/tests/node/index.mjs index 52670d48..360bf481 100644 --- a/tests/node/index.mjs +++ b/tests/node/index.mjs @@ -25,6 +25,7 @@ import "./tests/NodeDish.mjs"; import "./tests/Utils.mjs"; import "./tests/Categories.mjs"; import "./tests/lib/BigIntUtils.mjs"; +import "./tests/lib/ChartsProtocolPrototypePollution.mjs"; const testStatus = { allTestsPassing: true, diff --git a/tests/node/tests/lib/ChartsProtocolPrototypePollution.mjs b/tests/node/tests/lib/ChartsProtocolPrototypePollution.mjs new file mode 100644 index 00000000..be4e7667 --- /dev/null +++ b/tests/node/tests/lib/ChartsProtocolPrototypePollution.mjs @@ -0,0 +1,90 @@ +import TestRegister from "../../../lib/TestRegister.mjs"; +import {getSeriesValues} from "../../../../src/core/lib/Charts.mjs"; +import {objToTable} from "../../../../src/core/lib/Protocol.mjs"; +import SeriesChart from "../../../../src/core/operations/SeriesChart.mjs"; +import ParseUDP from "../../../../src/core/operations/ParseUDP.mjs"; +import it from "../../assertionHandler.mjs"; +import assert from "assert"; + +const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); + +TestRegister.addApiTests([ + it("Charts: should not pollute Object.prototype from a __proto__ series name", () => { + const xVal = ""; + delete Object.prototype[xVal]; + + try { + const result = getSeriesValues(`__proto__,${xVal},1`, "\n", ",", false); + + assert.equal(Object.prototype[xVal], undefined); + assert.deepEqual(result.xValues, [xVal]); + assert.equal(result.series.length, 1); + assert.equal(result.series[0].name, "__proto__"); + assert.equal(Object.getPrototypeOf(result.series[0].data), null); + assert(hasOwn(result.series[0].data, xVal)); + assert.equal(result.series[0].data[xVal], 1); + } finally { + delete Object.prototype[xVal]; + } + }), + + it("Charts: should keep __proto__ x-axis names as own data keys", () => { + const result = getSeriesValues("safe,__proto__,1", "\n", ",", false); + + assert.equal(result.series.length, 1); + assert.equal(Object.getPrototypeOf(result.series[0].data), null); + assert(hasOwn(result.series[0].data, "__proto__")); + assert.equal(result.series[0].data.__proto__, 1); + }), + + it("Protocol: should ignore inherited properties when rendering tables", () => { + const inheritedKey = ""; + delete Object.prototype[inheritedKey]; + + try { + Object.prototype[inheritedKey] = "polluted"; + + const html = objToTable({safe: "value"}); + + assert(!html.includes(inheritedKey)); + assert(!html.includes("polluted")); + assert(html.includes("safe")); + assert(html.includes("value")); + } finally { + delete Object.prototype[inheritedKey]; + } + }), + + it("Protocol: should escape table keys and scalar values", () => { + const obj = { + "field": "", + }; + + const html = objToTable(obj); + + assert(!html.includes("field")); + assert(!html.includes("")); + assert(html.includes("<b>field</b>")); + assert(html.includes("<img src=x onerror=alert(1)>")); + }), + + it("Series chart and Parse UDP: should not expose polluted prototype data as HTML", () => { + const xVal = ""; + delete Object.prototype[xVal]; + + try { + const chartHtml = new SeriesChart().run( + `__proto__,${xVal},1`, + ["Line feed", "Comma", "", 1, "red"] + ); + assert.equal(Object.prototype[xVal], undefined); + + const parseUDP = new ParseUDP(); + const tableHtml = parseUDP.present(parseUDP.run(chartHtml, ["Raw"])); + + assert(!/ Date: Wed, 17 Jun 2026 12:12:16 +0100 Subject: [PATCH 04/81] Bump v11.2.0 (#2570) --- CHANGELOG.md | 25 +++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b609c201..77a55714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,17 @@ All major and minor version changes will be documented in this file. Details of ## Details +### [11.2.0] - 2026-06-17 +This release includes a security fix ([#2569]) +- Security: Chart operation prototype protection [@C85297] | [#2569] +- Update website references [@C85297] | [#2566] +- Fix: Add input validation for XOR Checksum blocksize (#2537) [@dweep-js] | [#2542] +- Fix: Reverse highlights unwind incorrectly [@kendallgoto] [@C85297] | [#2022] +- Fix Uint8Array concat crash in Parse IPv4 header [@Zish19] | [#2409] +- Fix typos and documentation errors (bytes→bits, wrong release link, spelling) [@qa2me] [@GCHQDeveloper581] | [#2404] +- Add integer check for alphabet size [@heapframe] [@GCHQDeveloper581] | [#2458] +- fix: validate hexdump width upper bound [@skyswordw] | [#2514] + ### [11.1.0] - 2026-06-13 This release includes a security fix ([#2557]) - Security: Add fix, and tests, for Lorem Ipsum DoS issue [@GCHQDeveloper581] | [#2557] @@ -707,6 +718,7 @@ Breaking changes: ## [4.0.0] - 2016-11-28 - Initial open source commit [@n1474335] | [b1d73a72](https://github.com/gchq/CyberChef/commit/b1d73a725dc7ab9fb7eb789296efd2b7e4b08306) +[11.2.0]: https://github.com/gchq/CyberChef/releases/tag/v11.2.0 [11.1.0]: https://github.com/gchq/CyberChef/releases/tag/v11.1.0 [11.0.0]: https://github.com/gchq/CyberChef/releases/tag/v11.0.0 [10.24.0]: https://github.com/gchq/CyberChef/releases/tag/v10.24.0 @@ -1001,6 +1013,11 @@ Breaking changes: [@Louis-Ladd]: https://github.com/Louis-Ladd [@Blank0120]: https://github.com/Blank0120 [@zachbowden]: https://github.com/zachbowden +[@dweep-js]: https://github.com/dweep-js +[@Zish19]: https://github.com/Zish19 +[@qa2me]: https://github.com/qa2me +[@heapframe]: https://github.com/heapframe +[@skyswordw]: https://github.com/skyswordw [8ad18b]: https://github.com/gchq/CyberChef/commit/8ad18bc7db6d9ff184ba3518686293a7685bf7b7 @@ -1364,4 +1381,12 @@ Breaking changes: [#2332]: https://github.com/gchq/CyberChef/pull/2332 [#2353]: https://github.com/gchq/CyberChef/pull/2353 [#2351]: https://github.com/gchq/CyberChef/pull/2351 +[#2569]: https://github.com/gchq/CyberChef/pull/2569 +[#2566]: https://github.com/gchq/CyberChef/pull/2566 +[#2542]: https://github.com/gchq/CyberChef/pull/2542 +[#2022]: https://github.com/gchq/CyberChef/pull/2022 +[#2409]: https://github.com/gchq/CyberChef/pull/2409 +[#2404]: https://github.com/gchq/CyberChef/pull/2404 +[#2458]: https://github.com/gchq/CyberChef/pull/2458 +[#2514]: https://github.com/gchq/CyberChef/pull/2514 diff --git a/package-lock.json b/package-lock.json index 5125275a..a36ceaad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cyberchef", - "version": "11.1.0", + "version": "11.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cyberchef", - "version": "11.1.0", + "version": "11.2.0", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 8439ed01..7b630155 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cyberchef", - "version": "11.1.0", + "version": "11.2.0", "description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.", "author": "GCHQ ", "homepage": "https://gchq.github.io/CyberChef", From 9e7803e802793805c91209ddc015eefe738f1437 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:14:12 +0100 Subject: [PATCH 05/81] chore (deps): bump dompurify from 3.4.8 to 3.4.9 (#2573) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index a36ceaad..b2bcaf3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,7 +39,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^9.0.0", - "dompurify": "^3.4.8", + "dompurify": "^3.4.9", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -8585,9 +8585,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", - "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", + "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" diff --git a/package.json b/package.json index 7b630155..10a4475f 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^9.0.0", - "dompurify": "^3.4.8", + "dompurify": "^3.4.9", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", From 65f47c0343af0782ca95edfcc72fe2dddc158d68 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 18:21:56 +0100 Subject: [PATCH 06/81] chore (deps): bump launch-editor from 2.13.1 to 2.14.1 (#2574) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2bcaf3e..779b1b6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12561,14 +12561,14 @@ } }, "node_modules/launch-editor": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.13.1.tgz", - "integrity": "sha512-lPSddlAAluRKJ7/cjRFoXUFzaX7q/YKI7yPHuEvSJVqoXvFnJov1/Ud87Aa4zULIbA9Nja4mSPK8l0z/7eV2wA==", + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", "dev": true, "license": "MIT", "dependencies": { "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" + "shell-quote": "^1.8.4" } }, "node_modules/lazystream": { From 0b9258b8fb2afc74d32845c105222f22cf404af2 Mon Sep 17 00:00:00 2001 From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com> Date: Thu, 18 Jun 2026 15:33:11 +0100 Subject: [PATCH 07/81] Fix operation description rendering (#2577) --- src/web/HTMLOperation.mjs | 3 ++- tests/browser/00_nightwatch.js | 26 ++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/web/HTMLOperation.mjs b/src/web/HTMLOperation.mjs index 725f0b5f..0ba0ffc8 100755 --- a/src/web/HTMLOperation.mjs +++ b/src/web/HTMLOperation.mjs @@ -56,9 +56,10 @@ class HTMLOperation { if (this.description) { const infoLink = this.infoURL ? `
${titleFromWikiLink(this.infoURL)}` : ""; + const content = Utils.escapeHtml(this.description + infoLink); html += ` data-container='body' data-toggle='popover' data-placement='right' - data-content="${this.description}${infoLink}" data-html='true' data-trigger='hover' + data-content="${content}" data-html='true' data-trigger='hover' data-boundary='viewport' role='button'`; } diff --git a/tests/browser/00_nightwatch.js b/tests/browser/00_nightwatch.js index e64b476b..a0f093ee 100644 --- a/tests/browser/00_nightwatch.js +++ b/tests/browser/00_nightwatch.js @@ -56,6 +56,32 @@ module.exports = { browser.expect.element("//li[contains(@class, 'operation') and text()='Play Media']").to.be.present; browser.expect.element("//li[contains(@class, 'operation') and text()='Disassemble x86']").to.be.present; browser.expect.element("//li[contains(@class, 'operation') and text()='Register']").to.be.present; + browser.expect.element("//li[contains(@class, 'operation') and text()='Escape Smart Characters']").to.be.present; + }, + + "Operation popover descriptions render HTML safely": browser => { + const favouritesCat = "//a[contains(@class, 'category-title') and contains(@data-target, '#catFavourites')]", + op = "//ul[@id='search-results']//li[contains(@class, 'operation') and contains(., 'Escape Smart Characters')]"; + + browser + .useCss() + .clearValue("#search") + .setValue("#search", "Escape Smart Characters") + .useXpath() + .waitForElementVisible(op, 1000) + .moveToElement(op, 10, 10) + .useCss() + .waitForElementVisible(".popover-body code:last-of-type", 1000) + .expect.element(".popover-body code:last-of-type").text.to.contain("\"Hello\" -- world..."); + + browser + .useCss() + .moveToElement("#operations .title", 1, 1) + .waitForElementNotPresent(".popover-body", 1000) + .clearValue("#search") + .useXpath() + .getLocationInView(favouritesCat) + .click(favouritesCat); }, "Recipe can be run": browser => { From 7413e911c2aa47ae36bc54e45ac444af7514cf30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 07:19:17 +0100 Subject: [PATCH 08/81] chore (deps): bump the docker-dependencies group with 2 updates (#2579) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5bab956e..f70ba966 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:24-alpine@sha256:fb71d01345f11b708a3553c66e7c74074f2d506400ea81973343d915cb64eef0 AS builder +FROM --platform=$BUILDPLATFORM node:24-alpine@sha256:156b55f92e98ccd5ef49578a8cea0df4679826564bad1c9d4ef04462b9f0ded6 AS builder WORKDIR /app @@ -27,7 +27,7 @@ RUN npm run build ######################################### # Package static build files into nginx # ######################################### -FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:37f356a5eba5d187365b4f59cd6cc29f1f922ad18146d554b576a80983377e6a AS cyberchef +FROM nginxinc/nginx-unprivileged:stable-alpine@sha256:fafa1102c789119971b3d83f9293f1ef5526bc73583a12e13ff5cd1299ed8b6c AS cyberchef LABEL maintainer="GCHQ " From 7d0501764881280045c78e93de875839cc562794 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:28:47 +0100 Subject: [PATCH 09/81] chore (deps): bump the patch-updates group with 5 updates (#2580) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> --- package-lock.json | 40 ++++++++++++++++++++-------------------- package.json | 10 +++++----- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index 779b1b6b..c399e118 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "assert": "^2.1.0", "avsc": "^5.7.9", "bcryptjs": "^3.0.3", - "bignumber.js": "^11.1.3", + "bignumber.js": "^11.1.4", "blakejs": "^1.2.1", "bootstrap": "4.6.2", "bootstrap-colorpicker": "^3.4.0", @@ -39,7 +39,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^9.0.0", - "dompurify": "^3.4.9", + "dompurify": "^3.4.11", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -85,7 +85,7 @@ "path": "^0.12.7", "popper.js": "^1.16.1", "process": "^0.11.10", - "protobufjs": "^8.6.2", + "protobufjs": "^8.6.4", "punycode.js": "^2.3.1", "qr-image": "^3.2.0", "reflect-metadata": "^0.2.2", @@ -116,7 +116,7 @@ "@babel/runtime": "^7.29.7", "@codemirror/commands": "^6.10.3", "@codemirror/language": "^6.12.3", - "@codemirror/search": "^6.7.0", + "@codemirror/search": "^6.7.1", "@codemirror/state": "^6.5.4", "@codemirror/view": "^6.43.1", "@puppeteer/browsers": "3.0.4", @@ -159,7 +159,7 @@ "terser": "^5.48.0", "webpack": "^5.107.2", "webpack-bundle-analyzer": "^5.3.0", - "webpack-dev-server": "^5.2.4", + "webpack-dev-server": "^5.2.5", "webpack-node-externals": "^3.0.0", "worker-loader": "^3.0.8" }, @@ -1876,9 +1876,9 @@ } }, "node_modules/@codemirror/search": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", - "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz", + "integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==", "dev": true, "license": "MIT", "dependencies": { @@ -5794,9 +5794,9 @@ } }, "node_modules/bignumber.js": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.3.tgz", - "integrity": "sha512-+esZiNSo6VgFokTsYX6mYqNJfFd/IczzZCd4Z7cR8e+AQWhvIcj6nqQ1h9814D9u/TApU0jjTVmfWL0Pd1ZBdA==", + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.4.tgz", + "integrity": "sha512-AJ9dSeaUGj2xu7tEwmdqb51dqdb633xo4njI9K8ZFfcLrNr0XN8/EPkkZUNaF9fkCblGt2zVwZymesUdGynEkQ==", "license": "MIT" }, "node_modules/binary-extensions": { @@ -8585,9 +8585,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", - "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -15086,9 +15086,9 @@ "license": "MIT" }, "node_modules/protobufjs": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.2.tgz", - "integrity": "sha512-CCERJxzRvKMeEdJSLwdQf40TXWNPc8M4RkN7j/lxY6FQB+4do8rETWqj60AqxP9n0XIsxnSefZ8uhAaGKg2njw==", + "version": "8.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-8.6.4.tgz", + "integrity": "sha512-/+XMv9JalknuncEJSwsyEVlwcxVLKx2iaoSUXFZA86MJkdqyOdfrlB1sB7S6aKyUk9tl20YY+SgQe5J2sJHTcg==", "license": "BSD-3-Clause", "dependencies": { "long": "^5.3.2" @@ -18112,9 +18112,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.4.tgz", - "integrity": "sha512-GqDPGZN9bRqKBTkp4aWkobDDHMsrXKoGSdOH56smIri8qR0JG8gfL8/v/f/OZR3/OKXjG8uwJbFVhKm/FNU/UA==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz", + "integrity": "sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 10a4475f..f20fba60 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@babel/runtime": "^7.29.7", "@codemirror/commands": "^6.10.3", "@codemirror/language": "^6.12.3", - "@codemirror/search": "^6.7.0", + "@codemirror/search": "^6.7.1", "@codemirror/state": "^6.5.4", "@codemirror/view": "^6.43.1", "@puppeteer/browsers": "3.0.4", @@ -89,7 +89,7 @@ "terser": "^5.48.0", "webpack": "^5.107.2", "webpack-bundle-analyzer": "^5.3.0", - "webpack-dev-server": "^5.2.4", + "webpack-dev-server": "^5.2.5", "webpack-node-externals": "^3.0.0", "worker-loader": "^3.0.8" }, @@ -105,7 +105,7 @@ "assert": "^2.1.0", "avsc": "^5.7.9", "bcryptjs": "^3.0.3", - "bignumber.js": "^11.1.3", + "bignumber.js": "^11.1.4", "blakejs": "^1.2.1", "bootstrap": "4.6.2", "bootstrap-colorpicker": "^3.4.0", @@ -123,7 +123,7 @@ "d3": "7.9.0", "d3-hexbin": "^0.2.2", "diff": "^9.0.0", - "dompurify": "^3.4.9", + "dompurify": "^3.4.11", "es6-promisify": "^7.0.0", "escodegen": "^2.1.0", "esprima": "^4.0.1", @@ -169,7 +169,7 @@ "path": "^0.12.7", "popper.js": "^1.16.1", "process": "^0.11.10", - "protobufjs": "^8.6.2", + "protobufjs": "^8.6.4", "punycode.js": "^2.3.1", "qr-image": "^3.2.0", "reflect-metadata": "^0.2.2", From 43f19c04949a48d9380bb899a3e80df17b7576c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 09:46:59 +0100 Subject: [PATCH 10/81] chore (deps): bump form-data from 4.0.5 to 4.0.6 (#2572) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index c399e118..46d2fa53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9865,17 +9865,17 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -10907,9 +10907,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" From e18441d43086119b288e064d61ba5b090363b54e Mon Sep 17 00:00:00 2001 From: Shailendra Singh <84718204+Shailendra1703@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:26:11 +0530 Subject: [PATCH 11/81] Fix: added viewport styles to img tag in RenderImage Dish (#2109) Co-authored-by: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com> --- src/web/stylesheets/layout/_io.css | 8 ++++++++ tests/browser/02_ops.js | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index 0146bf27..abca9d84 100755 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -24,6 +24,14 @@ height: 100%; user-select: auto; } + +#output-html > img { + display: block; + max-width: 100%; + max-height: 100%; + margin: auto; +} + #output-text.html-output .cm-line .cm-widgetBuffer, #output-text.html-output .cm-line>br { display: none; diff --git a/tests/browser/02_ops.js b/tests/browser/02_ops.js index e295f08c..9139a53a 100644 --- a/tests/browser/02_ops.js +++ b/tests/browser/02_ops.js @@ -492,7 +492,22 @@ function testOpImage(browser, opName, filename, args=[]) { browser .waitForElementVisible("#output-html img") - .expect.element("#output-html img").to.have.css("width").which.matches(/^[^0]\d*px/); + .expect.element("#output-html img").to.have.css("width").which.matches(/^(?!0+(?:\.0+)?px$)\d+(?:\.\d+)?px$/); + + browser.execute(function() { + const output = document.getElementById("output-html"); + const img = output.querySelector("img"); + const outputRect = output.getBoundingClientRect(); + const imgRect = img.getBoundingClientRect(); + + return { + imageFitsWidth: imgRect.width <= outputRect.width, + imageFitsHeight: imgRect.height <= outputRect.height, + }; + }, [], function({value}) { + browser.expect(value.imageFitsWidth).to.be.equal(true); + browser.expect(value.imageFitsHeight).to.be.equal(true); + }); } /** @function From 33c2207427baec8713fed4401c451dffc8fbe4f3 Mon Sep 17 00:00:00 2001 From: MAN$I VERMA Date: Fri, 19 Jun 2026 17:29:04 +0530 Subject: [PATCH 12/81] feat: Add automated parameter validation framework (#2561) --- src/core/Ingredient.mjs | 66 ++++++++++ src/core/Operation.mjs | 19 +++ src/core/Recipe.mjs | 2 + src/core/config/Categories.json | 3 +- .../operations/AutomatedValidationTestOp.mjs | 78 +++++++++++ src/node/api.mjs | 3 + .../operations/tests/AutomatedValidation.mjs | 121 ++++++++++++++++++ tests/operations/tests/Hexdump.mjs | 2 +- 8 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 src/core/operations/AutomatedValidationTestOp.mjs create mode 100644 tests/operations/tests/AutomatedValidation.mjs diff --git a/src/core/Ingredient.mjs b/src/core/Ingredient.mjs index 0dd31707..4f81f83b 100644 --- a/src/core/Ingredient.mjs +++ b/src/core/Ingredient.mjs @@ -32,6 +32,8 @@ class Ingredient { this.min = null; this.max = null; this.step = 1; + this.integer = false; + this.allowEmpty = true; if (ingredientConfig) { this._parseConfig(ingredientConfig); @@ -59,6 +61,70 @@ class Ingredient { this.min = ingredientConfig.min; this.max = ingredientConfig.max; this.step = ingredientConfig.step; + this.integer = typeof ingredientConfig.integer !== "undefined" ? !!ingredientConfig.integer : false; + this.allowEmpty = typeof ingredientConfig.allowEmpty !== "undefined" ? !!ingredientConfig.allowEmpty : true; + } + + + /** + * Validates the given value against the constraints of this ingredient. + * + * @param {*} val + * @returns {boolean} + */ + validate(val) { + if (this.disabled) return true; + + let checkVal = val; + if (this.type === "toggleString" && val && typeof val === "object" && "string" in val) { + checkVal = val.string; + } + + // 1. check if empty + let isEmpty = false; + if (checkVal === null || checkVal === undefined || checkVal === "") { + isEmpty = true; + } else if (typeof checkVal.length === "number" && checkVal.length === 0) { + isEmpty = true; + } + + if (isEmpty) { + if (this.allowEmpty === false) { + throw new OperationError(`${this.name} cannot be empty.`); + } + return true; + } + + // 2. maxLength check + if (typeof this.maxLength === "number" && checkVal !== null && checkVal !== undefined) { + if (typeof checkVal === "string" && checkVal.length > this.maxLength) { + throw new OperationError(`${this.name} length cannot exceed ${this.maxLength}.`); + } + if (Array.isArray(checkVal) && checkVal.length > this.maxLength) { + throw new OperationError(`${this.name} length cannot exceed ${this.maxLength}.`); + } + if (checkVal instanceof Uint8Array && checkVal.length > this.maxLength) { + throw new OperationError(`${this.name} length cannot exceed ${this.maxLength}.`); + } + } + + // 3. number checks + if (this.type === "number") { + if (val === null || val === undefined || isNaN(val)) { + throw new OperationError(`${this.name} must be a number.`); + } + if (this.integer && !Number.isInteger(val)) { + throw new OperationError(`${this.name} must be an integer.`); + } + if (typeof this.min === "number" && val < this.min) { + throw new OperationError(`${this.name} must be greater than or equal to ${this.min}.`); + } + if (typeof this.max === "number" && val > this.max) { + throw new OperationError(`${this.name} must be less than or equal to ${this.max}.`); + } + } + + return true; } diff --git a/src/core/Operation.mjs b/src/core/Operation.mjs index 09058766..b35a49a6 100755 --- a/src/core/Operation.mjs +++ b/src/core/Operation.mjs @@ -189,11 +189,30 @@ class Operation { if (typeof ing.min === "number") conf.min = ing.min; if (typeof ing.max === "number") conf.max = ing.max; if (ing.step) conf.step = ing.step; + if (typeof ing.integer !== "undefined") conf.integer = ing.integer; + if (typeof ing.allowEmpty !== "undefined") conf.allowEmpty = ing.allowEmpty; return conf; }); } + /** + * Validates the operation's ingredients against their defined constraints. + * + * @param {Object[]} [args] - Optional list of argument values to validate. If not provided, validates the current ingredient values. + * @returns {boolean} - True if valid, throws an OperationError if invalid. + */ + validateIngredients(args) { + const values = args || this.ingValues; + this._ingList.forEach((ing, i) => { + if (i < values.length) { + ing.validate(values[i]); + } + }); + return true; + } + + /** * Returns the value of the Operation as it should be displayed in a recipe config. * diff --git a/src/core/Recipe.mjs b/src/core/Recipe.mjs index 84c91d61..0886e994 100755 --- a/src/core/Recipe.mjs +++ b/src/core/Recipe.mjs @@ -212,6 +212,8 @@ class Recipe { self.sendProgressMessage(i + 1, this.opList.length); } + op.validateIngredients(op.ingValues); + if (op.flowControl) { // Package up the current state let state = { diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index ceecd005..bce89d4c 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -584,7 +584,8 @@ "HTML To Text", "Generate Lorem Ipsum", "Numberwang", - "XKCD Random Number" + "XKCD Random Number", + "Automated Validation Test Op" ] }, { diff --git a/src/core/operations/AutomatedValidationTestOp.mjs b/src/core/operations/AutomatedValidationTestOp.mjs new file mode 100644 index 00000000..315eb417 --- /dev/null +++ b/src/core/operations/AutomatedValidationTestOp.mjs @@ -0,0 +1,78 @@ +/** + * @author CyberChef + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ + +import Operation from "../Operation.mjs"; + +/** + * Automated validation test operation + */ +class AutomatedValidationTestOp extends Operation { + + /** + * AutomatedValidationTestOp constructor + */ + constructor() { + super(); + + this.name = "Automated Validation Test Op"; + this.module = "Default"; + this.description = "Operation used specifically to test automated parameter validation."; + this.inputType = "string"; + this.outputType = "string"; + this.args = [ + { + "name": "Integer Number", + "type": "number", + "value": 5, + "min": 5, + "max": 10, + "integer": true + }, + { + "name": "Real Number", + "type": "number", + "value": 1.5, + "min": 1.5, + "max": 5.5 + }, + { + "name": "Non Empty String", + "type": "string", + "value": "hello", + "maxLength": 5, + "allowEmpty": false + }, + { + "name": "Empty Allowed String", + "type": "string", + "value": "", + "allowEmpty": true + }, + { + "name": "Non Empty Toggle String", + "type": "toggleString", + "value": { + "option": "Option A", + "string": "test" + }, + "toggleValues": ["Option A", "Option B"], + "allowEmpty": false + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {string} + */ + run(input, args) { + return "Success"; + } + +} + +export default AutomatedValidationTestOp; diff --git a/src/node/api.mjs b/src/node/api.mjs index 8002a8ac..f41feb23 100644 --- a/src/node/api.mjs +++ b/src/node/api.mjs @@ -193,6 +193,8 @@ export function _wrap(OpClass) { wrapped = async (input, args=null) => { const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args); + opInstance.validateIngredients(transformedArgs); + // SPECIAL CASE for Magic. Other flowControl operations will // not work because the opList is not passed in. if (isFlowControl) { @@ -229,6 +231,7 @@ export function _wrap(OpClass) { */ wrapped = (input, args=null) => { const {transformedInput, transformedArgs} = prepareOp(opInstance, input, args); + opInstance.validateIngredients(transformedArgs); const result = opInstance.run(transformedInput, transformedArgs); return new NodeDish({ value: result, diff --git a/tests/operations/tests/AutomatedValidation.mjs b/tests/operations/tests/AutomatedValidation.mjs new file mode 100644 index 00000000..da84de11 --- /dev/null +++ b/tests/operations/tests/AutomatedValidation.mjs @@ -0,0 +1,121 @@ +/** + * Automated Parameter Validation tests + * + * @author CyberChef + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + +TestRegister.addTests([ + { + name: "Automated Validation: Valid values", + input: "test", + expectedOutput: "Success", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Integer Number under min limit", + input: "test", + expectedOutput: "Integer Number must be greater than or equal to 5.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [4, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Integer Number over max limit", + input: "test", + expectedOutput: "Integer Number must be less than or equal to 10.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [11, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Integer Number not an integer", + input: "test", + expectedOutput: "Integer Number must be an integer.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5.5, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Real Number under min limit", + input: "test", + expectedOutput: "Real Number must be greater than or equal to 1.5.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.4, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Real Number over max limit", + input: "test", + expectedOutput: "Real Number must be less than or equal to 5.5.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 5.6, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Non Empty String over maxLength limit", + input: "test", + expectedOutput: "Non Empty String length cannot exceed 5.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "helloooo", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Non Empty String is empty", + input: "test", + expectedOutput: "Non Empty String cannot be empty.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Empty Allowed String is empty (allowed)", + input: "test", + expectedOutput: "Success", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "hello", "", { "option": "Option A", "string": "test" }] + } + ] + }, + { + name: "Automated Validation: Non Empty Toggle String is empty", + input: "test", + expectedOutput: "Non Empty Toggle String cannot be empty.", + recipeConfig: [ + { + op: "Automated Validation Test Op", + args: [5, 1.5, "hello", "", { "option": "Option A", "string": "" }] + } + ] + } +]); diff --git a/tests/operations/tests/Hexdump.mjs b/tests/operations/tests/Hexdump.mjs index 12d04492..be071e23 100644 --- a/tests/operations/tests/Hexdump.mjs +++ b/tests/operations/tests/Hexdump.mjs @@ -129,7 +129,7 @@ TestRegister.addTests([ { name: "To Hexdump: Width too large", input: "H", - expectedOutput: "Width must be no more than 65536", + expectedOutput: "Width must be less than or equal to 65536.", recipeConfig: [ { op: "To Hexdump", From 4a70dff3f2c6aacd8a8bbca42c440082efe88069 Mon Sep 17 00:00:00 2001 From: GCHQ Developer 85297 <95289555+C85297@users.noreply.github.com> Date: Fri, 19 Jun 2026 14:01:19 +0100 Subject: [PATCH 13/81] Fix URL encoding incorrectly converting input to UTF-8 (#2340) --- src/core/operations/URLEncode.mjs | 40 ++++++++++++---------- tests/operations/tests/URLEncodeDecode.mjs | 26 ++++++++++++++ 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/core/operations/URLEncode.mjs b/src/core/operations/URLEncode.mjs index a5efd213..99eec91d 100644 --- a/src/core/operations/URLEncode.mjs +++ b/src/core/operations/URLEncode.mjs @@ -21,7 +21,7 @@ class URLEncode extends Operation { this.module = "URL"; this.description = "Encodes problematic characters into percent-encoding, a format supported by URIs/URLs.

e.g. = becomes %3d"; this.infoURL = "https://wikipedia.org/wiki/Percent-encoding"; - this.inputType = "string"; + this.inputType = "byteArray"; this.outputType = "string"; this.args = [ { @@ -33,34 +33,38 @@ class URLEncode extends Operation { } /** - * @param {string} input + * @param {byteArray} input * @param {Object[]} args * @returns {string} */ run(input, args) { const encodeAll = args[0]; - return encodeAll ? this.encodeAllChars(input) : encodeURI(input); + return this.encodeBytes(input, encodeAll); } /** - * Encode characters in URL outside of encodeURI() function spec + * Encode bytes in URL using percent encoding. * - * @param {string} str + * @param {byteArray} bytes + * @param {boolean} encodeAll * @returns {string} */ - encodeAllChars (str) { - // TODO Do this programmatically - return encodeURIComponent(str) - .replace(/!/g, "%21") - .replace(/#/g, "%23") - .replace(/'/g, "%27") - .replace(/\(/g, "%28") - .replace(/\)/g, "%29") - .replace(/\*/g, "%2A") - .replace(/-/g, "%2D") - .replace(/\./g, "%2E") - .replace(/_/g, "%5F") - .replace(/~/g, "%7E"); + encodeBytes(bytes, encodeAll) { + const safeChars = encodeAll ? + /^[A-Za-z0-9]$/ : + /^[A-Za-z0-9:/?#[\]@!$&'()*+,;=%]$/; + + let output = ""; + + for (const byte of bytes) { + const char = String.fromCharCode(byte); + + output += safeChars.test(char) ? + char : + "%" + byte.toString(16).toUpperCase().padStart(2, "0"); + } + + return output; } } diff --git a/tests/operations/tests/URLEncodeDecode.mjs b/tests/operations/tests/URLEncodeDecode.mjs index 444f76d3..8d9d09db 100644 --- a/tests/operations/tests/URLEncodeDecode.mjs +++ b/tests/operations/tests/URLEncodeDecode.mjs @@ -89,4 +89,30 @@ TestRegister.addTests([ }, ], }, + { + name: "URLEncode: encodes UTF-8 text as UTF-8 bytes", + input: "你好", + expectedOutput: "%E4%BD%A0%E5%A5%BD", + recipeConfig: [ + { + op: "URL Encode", + args: [false], + }, + ], + }, + { + name: "URLEncode: preserves raw bytes from From Hex", + input: "6c6567697466696c6580000000000000000000000000000000000000000000000000000000000000000000000000000000000000000090746869737761737375706f736564746f6265616e6578706c6f6974", + expectedOutput: "legitfile%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%90thiswassuposedtobeanexploit", + recipeConfig: [ + { + op: "From Hex", + args: ["None"], + }, + { + op: "URL Encode", + args: [false], + }, + ], + }, ]); From ddbe9141322baf88b575ac1615b8e68d3c53b412 Mon Sep 17 00:00:00 2001 From: Shailendra Singh <84718204+Shailendra1703@users.noreply.github.com> Date: Fri, 19 Jun 2026 23:59:40 +0530 Subject: [PATCH 14/81] Added RenderPDF functionality (#2105) Co-authored-by: GCHQDeveloper581 <63102987+GCHQDeveloper581@users.noreply.github.com> very basic unit testing --- src/core/config/Categories.json | 4 +- src/core/operations/RenderPDF.mjs | 100 +++++++++++++++++++++++++++ tests/node/tests/nodeApi.mjs | 2 +- tests/operations/tests/RenderPDF.mjs | 37 ++++++++++ 4 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 src/core/operations/RenderPDF.mjs create mode 100644 tests/operations/tests/RenderPDF.mjs diff --git a/src/core/config/Categories.json b/src/core/config/Categories.json index bce89d4c..2879a13a 100644 --- a/src/core/config/Categories.json +++ b/src/core/config/Categories.json @@ -558,7 +558,7 @@ "Scatter chart", "Series chart", "Heatmap chart", - "Extract Audio Metadata" + "Render PDF" ] }, { @@ -603,4 +603,4 @@ "Comment" ] } -] +] \ No newline at end of file diff --git a/src/core/operations/RenderPDF.mjs b/src/core/operations/RenderPDF.mjs new file mode 100644 index 00000000..c1b6cfb5 --- /dev/null +++ b/src/core/operations/RenderPDF.mjs @@ -0,0 +1,100 @@ +/** + * @author Shailendra [singhshailendra.in] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + +import { fromBase64, toBase64 } from "../lib/Base64.mjs"; +import Operation from "../Operation.mjs"; +import OperationError from "../errors/OperationError.mjs"; +import Utils from "../Utils.mjs"; + +/** + * Render PDF operation + */ +class RenderPDF extends Operation { + + /** + * RenderPDF constructor + */ + constructor() { + super(); + + this.name = "Render PDF"; + this.module = "File"; + this.description = "Displays the input as a PDF preview. Supports Raw and Base64 input formats."; + this.inputType = "string"; + this.outputType = "byteArray"; + this.presentType = "html"; + this.args = [ + { + "name": "Input format", + "type": "option", + "value": ["Base64", "Raw"], + } + ]; + this.checks = [ + { + pattern: "^%PDF-", + flags: "", + args: ["Raw"], + useful: true, + output: { + mime: "application/pdf" + } + } + ]; + } + + /** + * @param {string} input + * @param {Object[]} args + * @returns {byteArray} + */ + run(input, args) { + const inputFormat = args[0]; + + if (!input.length) return []; + + // Convert input to raw bytes + switch (inputFormat) { + case "Base64": + input = fromBase64(input, undefined, "byteArray"); + break; + case "Raw": + default: + input = Utils.strToByteArray(input); + break; + } + + // Check PDF signature + if ( + input[0] !== 0x25 || // % + input[1] !== 0x50 || // P + input[2] !== 0x44 || // D + input[3] !== 0x46 // F + ) { + throw new OperationError("Input does not appear to be a PDF file."); + } + + return input; + } + + /** + * Displays the PDF using HTML for web apps. + * + * @param {byteArray} data + * @returns {html} + */ + async present(data) { + if (!data.length) return ""; + + const base64 = toBase64(data); + const dataURI = "data:application/pdf;base64," + base64; + + return ``; + } + +} + +export default RenderPDF; diff --git a/tests/node/tests/nodeApi.mjs b/tests/node/tests/nodeApi.mjs index 2510ef17..5f2476ee 100644 --- a/tests/node/tests/nodeApi.mjs +++ b/tests/node/tests/nodeApi.mjs @@ -136,7 +136,7 @@ TestRegister.addApiTests([ it("chef.help: returns multiple results", () => { const result = chef.help("base 64"); - assert.strictEqual(result.length, 13); + assert.strictEqual(result.length, 14); }), it("chef.help: looks in description for matches too", () => { diff --git a/tests/operations/tests/RenderPDF.mjs b/tests/operations/tests/RenderPDF.mjs new file mode 100644 index 00000000..f359aa20 --- /dev/null +++ b/tests/operations/tests/RenderPDF.mjs @@ -0,0 +1,37 @@ +/** + * RenderPDF tests. + * + * @copyright Crown Copyright 2026 + * @license Apache-2.0 + */ +import TestRegister from "../../lib/TestRegister.mjs"; + + +TestRegister.addTests([ + { + name: "RenderPDF", + input: "Not a PDF", + expectedOutput: "Input does not appear to be a PDF file.", + recipeConfig: [ + { + op: "Render PDF", + args: ["Raw"] + }, + ], + }, + { + name: "RenderPDF", + input: "", + expectedMatch: /^