fix(arg-parser): take the arguments as stdin text, and block a heredoc that closes early

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-26 15:40:21 +01:00
commit f1704f0800
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
4 changed files with 287 additions and 16 deletions

View file

@ -258,6 +258,43 @@ const deobfuscated = normalizeBashExpansion(command);
const normalized = normalizeCommand(deobfuscated);
const commandView = commandPositionView(deobfuscated.replace(/\x1B\[[0-9;]*m/g, ''));
// BEGIN args-heredoc-closed-early
// The commands hand $ARGUMENTS to lib/parsers/arg-parser.mjs (--stdin) through
// a quoted heredoc closed by VOYAGE_ARGS_EOF_7F3C9E. A typed line equal to that
// delimiter would close the heredoc there, and the shell would run every line
// after it as a command (fix round 4.5b, R6). This check reads the raw
// command: `normalized` and `commandView` collapse lines and drop heredoc
// bodies (hooks/lib/bash-view.mjs). Openers <<'D', <<"D" and <<\D are quoted
// (the body is not expanded); any other opener of the delimiter is treated as
// unquoted and blocked, since its body would expand. A quoted opener passes
// only when exactly one line, trimmed, equals the delimiter and every line
// after it is empty.
const ARGS_DELIM = 'VOYAGE_ARGS_EOF_7F3C9E';
const argsOpener = /<<-?\s*(['"\\]?)VOYAGE_ARGS_EOF_7F3C9E(['"]?)/.exec(command);
if (argsOpener) {
const [, open, close] = argsOpener;
const quoted = (open === '\\' && close === '') || (open !== '' && open !== '\\' && close === open);
const lines = command.split('\n');
const closing = [];
lines.forEach((line, i) => { if (line.trim() === ARGS_DELIM) closing.push(i); });
const closesOnceAtEnd = closing.length === 1 &&
lines.slice(closing[0] + 1).every((line) => line.trim() === '');
if (!quoted || !closesOnceAtEnd) {
const why = !quoted
? `The heredoc that hands the arguments to the arg parser opens ${ARGS_DELIM} unquoted, so the shell would expand its body. ` +
`Way forward: open it as <<'${ARGS_DELIM}'.`
: `The arguments hold a line equal to the heredoc's closing line ${ARGS_DELIM}, or text follows that closing line, ` +
'so the shell would run what follows as commands. Way forward: take that line out of the arguments and run the command again.';
process.stderr.write(
'[voyage] BLOCKED: args-heredoc-closed-early\n' +
` Command: ${normalized.slice(0, 200)}${normalized.length > 200 ? '...' : ''}\n` +
` ${why}\n`
);
process.exit(2);
}
}
// END args-heredoc-closed-early
// Check BLOCK rules first
for (const rule of BLOCK_RULES) {
if (rule.pattern.test(rule.commandView ? commandView : normalized)) {