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/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. diff --git a/package-lock.json b/package-lock.json index 5125275a..779b1b6b 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": { @@ -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" @@ -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": { diff --git a/package.json b/package.json index 8439ed01..10a4475f 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", @@ -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", 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/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 => { 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(!/