Skip to content

Commit 39a5ea1

Browse files
committed
playground: sanitize the ?cmd= URL parameter (fixes #52, #53)
Strip control / unprintable characters (keeping newline and tab) from the ?cmd= value before running it, so a bare NUL (%00) no longer reaches the parser and prints a baffling "command not found:" with nothing after it (#52), and cap the length so an oversized value degrades gracefully instead of being handed to the WASM runtime wholesale (#53). Adds sanitizeUrlCommand unit tests. Also tidies the file so it lints clean: drop the dead promptLen helper and use an optional catch binding.
1 parent ba9c08e commit 39a5ea1

2 files changed

Lines changed: 60 additions & 9 deletions

File tree

static/js/wasm-terminal.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,29 @@ function parseCommandLine(line) {
506506
return pipeline;
507507
}
508508

509+
// Upper bound on the length of a command supplied via the ?cmd= URL parameter.
510+
// Long enough for any real example, short enough that a pathological value
511+
// can't be handed to the WASM runtime wholesale (issue #53).
512+
const MAX_URL_COMMAND_LENGTH = 2048;
513+
514+
/**
515+
* Sanitize a command coming from the ?cmd= URL parameter before it is executed.
516+
*
517+
* Strips control / unprintable characters that cannot be typed at the prompt -
518+
* keeping only newline (used to separate multiple commands) and tab - so that
519+
* an invisible byte like NUL (%00) no longer reaches the parser and produces a
520+
* baffling "command not found:" with nothing after it (issue #52). Also caps
521+
* the length so an oversized value degrades gracefully (issue #53).
522+
*/
523+
function sanitizeUrlCommand(raw) {
524+
if (!raw) return "";
525+
// Remove C0 controls (0x00-0x1F) except tab (0x09) and newline (0x0A), plus DEL (0x7F).
526+
// eslint-disable-next-line no-control-regex -- stripping control chars is the whole point
527+
let cmd = raw.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
528+
if (cmd.length > MAX_URL_COMMAND_LENGTH) cmd = cmd.slice(0, MAX_URL_COMMAND_LENGTH);
529+
return cmd;
530+
}
531+
509532
/**
510533
* Read a file from the virtual filesystem. Returns its content as a string,
511534
* or null if not found.
@@ -804,12 +827,6 @@ function promptStr() {
804827
return `\x1b[1;38;5;166muutils\x1b[0m ${dir}\x1b[1;38;5;166m$\x1b[0m `;
805828
}
806829

807-
function promptLen() {
808-
// Visible character count (without ANSI escapes) for cursor positioning
809-
const dir = cwd ? `${cwd} ` : "";
810-
return `uutils ${dir}$ `.length;
811-
}
812-
813830
function prompt() {
814831
if (!terminal) return;
815832
terminal.write("\r\n" + promptStr());
@@ -1033,14 +1050,14 @@ async function initPlayground(containerId) {
10331050
terminal.writeln("Type \x1b[1;32mhelp\x1b[0m for available commands.");
10341051
terminal.writeln("Sample data files: names.txt, numbers.txt, fruits.txt, csv.txt, words.txt");
10351052
terminal.writeln("\x1b[2mgrep, find/locate/updatedb, sed and diff/cmp load on demand - just run them, or use the buttons above.\x1b[0m");
1036-
} catch (e) {
1053+
} catch {
10371054
terminal.writeln(" \x1b[1;31mfailed\x1b[0m");
10381055
terminal.writeln("Failed to load WASM binary. Commands are not available.");
10391056
terminal.writeln("Try reloading the page.");
10401057
}
10411058

10421059
// Run command(s) from URL ?cmd= parameter if present
1043-
const urlCmd = new URLSearchParams(window.location.search).get("cmd");
1060+
const urlCmd = sanitizeUrlCommand(new URLSearchParams(window.location.search).get("cmd"));
10441061
if (urlCmd) {
10451062
for (const cmd of urlCmd.split("\n")) {
10461063
if (cmd.trim()) await runInTerminal(cmd.trim());
@@ -1114,6 +1131,7 @@ window.programSize = async (group) => {
11141131
// Expose internals for testing
11151132
window._uutilsTestInternals = {
11161133
parseCommandLine,
1134+
sanitizeUrlCommand,
11171135
executeCommandLine,
11181136
resolvePath,
11191137
lookupDir,

static/js/wasm-terminal.test.html

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ <h1>wasm-terminal unit tests</h1>
2020
<script>
2121
(async function() {
2222
const T = window._uutilsTestInternals;
23-
const { parseCommandLine, executeCommandLine, resolvePath, lookupDir, getPersistentDir, readVirtualFile, writeVirtualFile, SAMPLE_FILES, AVAILABLE_COMMANDS } = T;
23+
const { parseCommandLine, sanitizeUrlCommand, executeCommandLine, resolvePath, lookupDir, getPersistentDir, readVirtualFile, writeVirtualFile, SAMPLE_FILES, AVAILABLE_COMMANDS } = T;
2424

2525
let passed = 0;
2626
let failed = 0;
@@ -142,6 +142,39 @@ <h1>wasm-terminal unit tests</h1>
142142
assert("pipe + redirect: stdout", p[1].stdout, "out.txt");
143143
assertDeep("pipe + redirect: second args", p[1].args, ["head", "-3"]);
144144

145+
// ===== sanitizeUrlCommand =====
146+
section("sanitizeUrlCommand");
147+
148+
const NUL = String.fromCharCode(0);
149+
const VT = String.fromCharCode(11); // vertical tab (%0B)
150+
151+
assert("null/empty input -> empty",
152+
sanitizeUrlCommand("") + sanitizeUrlCommand(null), "");
153+
154+
// issue #52: a bare NUL must not reach the parser as a command
155+
assert("lone NUL is stripped to empty",
156+
sanitizeUrlCommand(NUL), "");
157+
158+
assert("NUL embedded in command is removed",
159+
sanitizeUrlCommand("ec" + NUL + "ho hi"), "echo hi");
160+
161+
assert("vertical tabs are stripped",
162+
sanitizeUrlCommand(VT + VT + "ls" + VT), "ls");
163+
164+
assert("newline is preserved (multi-command separator)",
165+
sanitizeUrlCommand("echo a\necho b"), "echo a\necho b");
166+
167+
assert("tab is preserved",
168+
sanitizeUrlCommand("echo\ta"), "echo\ta");
169+
170+
assert("printable command is unchanged",
171+
sanitizeUrlCommand("seq 1 10 | factor"), "seq 1 10 | factor");
172+
173+
// issue #53: an oversized value is capped instead of handed off wholesale
174+
const huge = sanitizeUrlCommand((VT + "ls").repeat(9999));
175+
assert("oversized command is length-capped",
176+
huge.length <= 2048, true);
177+
145178
// ===== executeCommandLine: builtins =====
146179
section("executeCommandLine: builtins");
147180

0 commit comments

Comments
 (0)