Merge remote-tracking branch 'upstream/master'

This commit is contained in:
github-actions[bot] 2026-05-25 09:45:00 +00:00
commit 7830bac871
22 changed files with 682 additions and 301 deletions

View File

@ -25,12 +25,8 @@ updates:
# see issue #2214 for rationale for each of these
- dependency-name: '@xmldom/xmldom'
versions: [ '>=0.9.0' ]
- dependency-name: 'bcryptjs'
versions: [ '>=3.0.0' ]
- dependency-name: 'bootstrap'
versions: [ '>=5.0.0' ]
- dependency-name: 'bson'
versions: [ '>=5.0.0' ]
- dependency-name: 'cbor'
versions: [ '>=10.0.0' ]
- dependency-name: 'eslint'

View File

@ -44,9 +44,17 @@ jobs:
- name: Generate sitemap
run: npx grunt exec:sitemap
- name: Setup Chrome
id: setup-chrome
if: success()
run: |
npx @puppeteer/browsers install chrome@148
echo chromedir=$(dirname $(find `pwd`/chrome/* -name chrome -print -quit)) >> $GITHUB_OUTPUT
- name: UI Tests
if: success()
run: |
export PATH=${{ steps.setup-chrome.outputs.chromedir }}:$PATH
sudo apt-get install xvfb
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui

View File

@ -45,9 +45,17 @@ jobs:
path: build/prod/*.zip
retention-days: 5
- name: Setup Chrome
id: setup-chrome
if: success()
run: |
npx @puppeteer/browsers install chrome@148
echo chromedir=$(dirname $(find `pwd`/chrome/* -name chrome -print -quit)) >> $GITHUB_OUTPUT
- name: UI Tests
if: success()
run: |
export PATH=${{ steps.setup-chrome.outputs.chromedir }}:$PATH
sudo apt-get install xvfb
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
@ -61,6 +69,6 @@ jobs:
- name: Production Image Build
if: success()
id: build-image
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
platforms: linux/amd64,linux/arm64,linux/arm/v7

View File

@ -46,8 +46,17 @@ jobs:
- name: Production Build
run: npx grunt prod
- name: UI Tests
- name: Setup Chrome
id: setup-chrome
if: success()
run: |
npx @puppeteer/browsers install chrome@148
echo chromedir=$(dirname $(find `pwd`/chrome/* -name chrome -print -quit)) >> $GITHUB_OUTPUT
- name: UI Tests
if: success()
run: |
export PATH=${{ steps.setup-chrome.outputs.chromedir }}:$PATH
sudo apt-get install xvfb
xvfb-run --server-args="-screen 0 1200x800x24" npx grunt testui
@ -75,7 +84,7 @@ jobs:
password: ${{ env.REGISTRY_PASSWORD }}
- name: Publish to GHCR
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
with:
context: .
push: true

1
.gitignore vendored
View File

@ -13,3 +13,4 @@ src/node/index.mjs
**/*.DS_Store
tests/browser/output/*
.node-version
chrome

View File

@ -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 <oss@gchq.gov.uk>"

View File

@ -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`
]);
}

808
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -49,10 +49,11 @@
"@codemirror/search": "^6.7.0",
"@codemirror/state": "^6.5.4",
"@codemirror/view": "^6.43.0",
"@puppeteer/browsers": "3.0.3",
"autoprefixer": "^10.5.0",
"babel-loader": "^10.1.1",
"base64-loader": "^1.0.0",
"chromedriver": "^146.0.6",
"chromedriver": "^148.0.3",
"cli-progress": "^3.12.0",
"colors": "^1.4.0",
"compression-webpack-plugin": "^12.0.0",
@ -86,7 +87,7 @@
"prompt": "^1.3.0",
"sitemap": "^8.0.3",
"terser": "^5.46.2",
"webpack": "^5.106.2",
"webpack": "^5.107.1",
"webpack-bundle-analyzer": "^5.3.0",
"webpack-dev-server": "^5.0.4",
"webpack-node-externals": "^3.0.0",
@ -110,7 +111,7 @@
"bootstrap-colorpicker": "^3.4.0",
"bootstrap-material-design": "^4.1.3",
"browserify-zlib": "^0.2.0",
"bson": "^4.7.2",
"bson": "^7.2.0",
"buffer": "^6.0.3",
"cbor": "9.0.2",
"chi-squared": "^1.1.0",
@ -168,7 +169,7 @@
"path": "^0.12.7",
"popper.js": "^1.16.1",
"process": "^0.11.10",
"protobufjs": "^7.5.8",
"protobufjs": "^7.6.0",
"punycode.js": "^2.3.1",
"qr-image": "^3.2.0",
"reflect-metadata": "^0.2.2",
@ -177,7 +178,7 @@
"snackbarjs": "^1.1.0",
"sortablejs": "^1.15.7",
"split.js": "^1.6.5",
"sql-formatter": "^15.7.4",
"sql-formatter": "^15.8.0",
"ssdeep.js": "0.0.3",
"stream-browserify": "^3.0.0",
"tesseract.js": "^6.0.1",

View File

@ -5,7 +5,7 @@
*/
import Operation from "../Operation.mjs";
import bson from "bson";
import { deserialize } from "bson";
import OperationError from "../errors/OperationError.mjs";
/**
@ -37,7 +37,7 @@ class BSONDeserialise extends Operation {
if (!input.byteLength) return "";
try {
const data = bson.deserialize(new Buffer(input));
const data = deserialize(new Uint8Array(input));
return JSON.stringify(data, null, 2);
} catch (err) {
throw new OperationError(err.toString());

View File

@ -5,7 +5,7 @@
*/
import Operation from "../Operation.mjs";
import bson from "bson";
import { serialize } from "bson";
import OperationError from "../errors/OperationError.mjs";
/**
@ -38,7 +38,8 @@ class BSONSerialise extends Operation {
try {
const data = JSON.parse(input);
return bson.serialize(data).buffer;
const result = serialize(data);
return result.buffer.slice(result.byteOffset, result.byteOffset + result.byteLength);
} catch (err) {
throw new OperationError(err.toString());
}

View File

@ -92,7 +92,7 @@ class ParseEthernetFrame extends Operation {
const packetData = input.slice(offset);
if (outputFormat === "Packet data") {
return Utils.byteArrayToChars(packetData);
return Utils.escapeHtml(Utils.byteArrayToChars(packetData));
} else if (outputFormat === "Packet data (hex)") {
return toHex(packetData);
} else if (outputFormat === "Text output") {

View File

@ -138,7 +138,7 @@ class ParseIPv4Header extends Operation {
} else if (outputFormat === "Data (hex)") {
return toHex(data);
} else if (outputFormat === "Data (raw)") {
return Utils.byteArrayToChars(data);
return Utils.escapeHtml(Utils.byteArrayToChars(data));
}
}

View File

@ -6,7 +6,7 @@
import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import BSON from "bson";
import { ObjectId } from "bson";
/**
* Parse ObjectID timestamp operation
@ -35,7 +35,7 @@ class ParseObjectIDTimestamp extends Operation {
*/
run(input, args) {
try {
const objectId = new BSON.ObjectID(input);
const objectId = new ObjectId(input);
return objectId.getTimestamp().toISOString();
} catch (err) {
throw new OperationError(err);

View File

@ -15,6 +15,20 @@ import Utils from "../Utils.mjs";
const d3 = d3temp.default ? d3temp.default : d3temp;
const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
/**
* Removes D3's internal bound data from a nodom tree before serialization.
* nodom serializes enumerable expando properties such as __data__ as attributes,
* so leaving them on attacker-controlled values can create executable markup.
*
* @param {Object} node
*/
function clearD3BoundData(node) {
delete node.__data__;
if (!node.childNodes) return;
node.childNodes.forEach(clearD3BoundData);
}
/**
* Series chart operation
*/
@ -222,6 +236,8 @@ class SeriesChart extends Operation {
.text(serie.name);
});
clearD3BoundData(svg.node());
return svg._groups[0][0].outerHTML;
}

View File

@ -42,6 +42,14 @@ class App {
this.progress = 0;
this.ingId = 0;
// stateChangeId increments on every statechange dispatch; bakeStateId records
// the stateChangeId captured when the most recent bake started. autoBake uses
// these to decide whether the output is genuinely stale, so a debounced
// autoBake firing shortly after a manual bake doesn't clobber the bake's
// hideStaleIndicator with a redundant showStaleIndicator.
this.stateChangeId = 0;
this.bakeStateId = -1;
this.appLoaded = false;
this.workerLoaded = false;
this.waitersLoaded = false;
@ -136,6 +144,9 @@ class App {
bake(step=false) {
if (this.baking) return;
// Record which state version this bake is covering.
this.bakeStateId = this.stateChangeId;
// Reset attemptHighlight flag
this.options.attemptHighlight = true;
@ -166,7 +177,10 @@ class App {
nums: [this.manager.tabs.getActiveTab("input")],
step: false
});
} else {
} else if (this.bakeStateId < this.stateChangeId) {
// Only show stale-indicator if the most recent bake didn't cover the
// current state. Without this guard, a debounced autoBake firing after
// a manual bake completed would re-show the indicator on fresh output.
this.manager.controls.showStaleIndicator();
}
}
@ -768,6 +782,10 @@ class App {
* @param {event} e
*/
stateChange(e) {
// Bump the state-change counter synchronously so a manual bake invoked between
// here and the debounced autoBake firing can record it via bakeStateId.
this.stateChangeId++;
debounce(function() {
this.progress = 0;
this.autoBake();

View File

@ -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.
</p>
<h6>CyberChef v<%= htmlWebpackPlugin.options.version %></h6>
<h6>CyberChef <%= htmlWebpackPlugin.options.version %></h6>
<ul>
<li>Build time: <%= htmlWebpackPlugin.options.compileTime %></li>
<li>The changelog for this version can be viewed <a href="https://github.com/gchq/CyberChef/blob/v<%= htmlWebpackPlugin.options.version %>/CHANGELOG.md">here</a></li>
<li>The changelog for this version can be viewed <a href="https://github.com/gchq/CyberChef/blob/v<%= htmlWebpackPlugin.options.latestReleaseVersion %>/CHANGELOG.md">here</a></li>
<li>&copy; Crown Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %></li>
<li>Released under the Apache Licence, Version 2.0</li>
<li>SHA256 hash: DOWNLOAD_HASH_PLACEHOLDER</li>
</ul>
<a href="CyberChef_v<%= htmlWebpackPlugin.options.version %>.zip" download class="btn btn-outline-primary">Download ZIP file</a>
<a href="<%= htmlWebpackPlugin.options.downloadZipFilename %>" download class="btn btn-outline-primary">Download ZIP file</a>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal">Ok</button>

View File

@ -51,14 +51,17 @@ function setInput(browser, input, type=true) {
*/
function bake(browser) {
browser
// Let any pending debounced inputChange/stateChange (~20ms each) fire so the
// worker has the latest input buffer before we ask it to bake.
.pause(50)
// Ensure we're not currently busy
.waitForElementNotVisible("#output-loader", 5000)
.waitForElementNotVisible("#output-loader", 10000)
.expect.element("#bake span").text.to.equal("BAKE!");
browser
.click("#bake")
.waitForElementNotVisible("#stale-indicator", 5000)
.waitForElementNotVisible("#output-loader", 5000);
.waitForElementNotVisible("#stale-indicator", 10000)
.waitForElementNotVisible("#output-loader", 10000);
}
/** @function

View File

@ -126,6 +126,7 @@ import "./tests/NormaliseUnicode.mjs";
import "./tests/NTLM.mjs";
import "./tests/OTP.mjs";
import "./tests/ParseEthernetFrame.mjs";
import "./tests/ParseIPv4Header.mjs";
import "./tests/ParseIPRange.mjs";
import "./tests/ParseObjectIDTimestamp.mjs";
import "./tests/ParseQRCode.mjs";

View File

@ -41,6 +41,17 @@ TestRegister.addTests([
}
],
},
{
name: "Series chart escapes x-axis values in serialized SVG",
input: `s,x"><script>globalThis.seriesChartInjected=1</script><g a=",1`,
unexpectedMatch: /<script>|__data__=/,
recipeConfig: [
{
"op": "Series chart",
"args": ["Line feed", "Comma", "", 1, "red"]
}
],
},
{
name: "Heatmap chart",
input: "100 100\n200 200\n300 300\n400 400\n500 500",

View File

@ -41,5 +41,16 @@ TestRegister.addTests([
"args": ["Hex", "Text output"]
}
]
},
{
name: "Parse Ethernet frame escapes packet data HTML",
input: "000000000000ffffffffffff08003c696d67207372633d78206f6e6572726f723d616c6572742831293e3c7363726970743e616c6572742832293c2f7363726970743e",
expectedOutput: "&lt;img src=x onerror=alert(1)&gt;&lt;script&gt;alert(2)&lt;/script&gt;",
recipeConfig: [
{
"op": "Parse Ethernet frame",
"args": ["Hex", "Packet data"]
}
]
}
]);

View File

@ -0,0 +1,23 @@
/**
* Parse IPv4 header tests.
*
* @author C85297 [95289555+C85297@users.noreply.github.com]
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
name: "Parse IPv4 header: Correctly formatted HTML output",
input: "45 00 00 3c 1c 46 40 00 40 06 b1 e6 c0 a8 00 01 c0 a8 00 02 3c 73 63 72 69 70 74 3e 61 6c 65 72 74 28 31 33 33 37 29 3c 2f 73 63 72 69 70 74 3e",
expectedOutput: "&lt;script&gt;alert(1337)&lt;/script&gt;",
recipeConfig: [
{
op: "Parse IPv4 header",
args: ["Hex", "Data (raw)"]
}
]
}
]);