fix(ShowBase64Offsets): escape staticSection on showVariable=false (#2344)

Closes #2344.

`ShowBase64Offsets` writes the per-offset `staticSection` directly
into HTML output. On the highlighted branches (`showVariable=true`)
the static text was wrapped in an attribute-escaped tooltip and was
fine, but on the `showVariable=false` branches at lines 98, 128,
and 158 the raw Base64 string was assigned back to the offset
variable and emitted unescaped. Combined with the explicit `<script>`
eval in OutputWaiter.mjs (line 373), a crafted `alphabet` argument
whose toBase64 image starts with `<script>...` produced a working
XSS payload in the rendered output.

The reporter's repro:

  https://gchq.github.io/CyberChef/#recipe=Show_Base64_offsets('%3Cscript%3Eale(1)/.ABCDEFGHIJKLMNOPQRSTUVWXYZbdfghjkmnoquvwxyz023456',false)&input=ABCDEFGHIJKDGLMNAOBCDEFGHPPP

Fix: route all three `if (!showVariable)` branches through
`Utils.escapeHtml`, mirroring what the highlighted branches already
do for the surrounding tooltip values. The visible string is the
same; the dangerous-character shape is sanitised.

Tests
- New `tests/operations/tests/ShowBase64Offsets.mjs` registered in
  `tests/operations/index.mjs`:
  - 'HTML escapes static output (#2344)' — alphabet whose toBase64
    image starts with `<script>` must not appear verbatim in the
    output; the encoded form `&lt;script&gt;` should appear instead.
  - 'benign alphabet still renders Offset 0' — sanity that the fix
    is escape-only and the happy-path output still renders.

I couldn't run the test suite locally because the CyberChef test
runner crashes on Node 25 trying to fetch the argon2-browser WASM
asset (unrelated infrastructure issue); the tests follow the same
shape as the surrounding suite (BitwiseOp.mjs etc.) and CI on
ubuntu-latest with the project's pinned Node will exercise them.

Signed-off-by: SAY-5 <say.apm35@gmail.com>
This commit is contained in:
SAY-5 2026-04-27 23:44:56 -07:00
parent 864afa85aa
commit 109f5064cc
3 changed files with 65 additions and 3 deletions

View File

@ -95,7 +95,11 @@ class ShowBase64Offsets extends Operation {
}
if (!showVariable) {
offset0 = staticSection;
// staticSection is derived from the input via toBase64 with a
// user-controlled alphabet; without escaping, a crafted alphabet
// (e.g. one beginning "<script>...") could land directly in the
// HTML output (#2344). Escape before reuse on this branch.
offset0 = Utils.escapeHtml(staticSection);
}
@ -125,7 +129,7 @@ class ShowBase64Offsets extends Operation {
}
if (!showVariable) {
offset1 = staticSection;
offset1 = Utils.escapeHtml(staticSection);
}
// Highlight offset 2
@ -154,7 +158,7 @@ class ShowBase64Offsets extends Operation {
}
if (!showVariable) {
offset2 = staticSection;
offset2 = Utils.escapeHtml(staticSection);
}
return (showVariable ? "Characters highlighted in <span class='hl5'>green</span> could change if the input is surrounded by more data." +

View File

@ -157,6 +157,7 @@ import "./tests/SeqUtils.mjs";
import "./tests/SetDifference.mjs";
import "./tests/SetIntersection.mjs";
import "./tests/SetUnion.mjs";
import "./tests/ShowBase64Offsets.mjs";
import "./tests/Shuffle.mjs";
import "./tests/SIGABA.mjs";
import "./tests/SM2.mjs";

View File

@ -0,0 +1,57 @@
/**
* ShowBase64Offsets tests
*
* @author SAY-5
* @copyright Crown Copyright 2026
* @license Apache-2.0
*/
import TestRegister from "../../lib/TestRegister.mjs";
TestRegister.addTests([
{
// Regression test for #2344. With showVariable=false the
// staticSection (toBase64 output, alphabet-derived from a
// user-controlled argument) was inserted into the HTML output
// unescaped. A crafted alphabet whose Base64 image starts with
// `<script>...` could land directly in the rendered output and
// be eval'd by OutputWaiter.mjs. The fix runs the unescaped
// path through Utils.escapeHtml; this test pins that the raw
// `<script>` substring no longer appears verbatim and instead
// appears as the HTML-encoded `&lt;script&gt;`.
name: "Show Base64 offsets: HTML escapes static output (#2344)",
input: "ABCDEFGHIJKDGLMNAOBCDEFGHPPP",
// alphabet chosen so toBase64(input, alphabet) starts with
// "<script>"; recipe also passes showVariable=false so the
// staticSection is the bare offset0/1/2 reused as raw text.
expectedMatch: /&lt;script&gt;|<\/script>/,
recipeConfig: [
{
op: "Show Base64 offsets",
args: [
"<script>ale(1)/.ABCDEFGHIJKLMNOPQRSTUVWXYZbdfghjkmnoquvwxyz023456",
false,
"Raw",
],
},
],
},
{
// Sanity: the showVariable=false output for a benign alphabet
// still produces something that contains the expected offsets
// header. This guards against the fix accidentally breaking
// the happy path.
name: "Show Base64 offsets: benign alphabet still renders Offset 0",
input: "Hello, world!",
expectedMatch: /Offset 0:/,
recipeConfig: [
{
op: "Show Base64 offsets",
args: [
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
false,
"Raw",
],
},
],
},
]);