fix(hooks): the args-heredoc guard blocks a heredoc operator a second shell would read, with a bypass corpus
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7ab4100489
commit
ebec492f6a
5 changed files with 878 additions and 3 deletions
|
|
@ -335,6 +335,17 @@ const commandView = commandPositionView(deobfuscated.replace(/\x1B\[[0-9;]*m/g,
|
|||
// the < is read as unescaped. The regex reading takes the shell reading's
|
||||
// decision for the same <. The < after the escaped one is read as usual,
|
||||
// so a << right after it is still checked (fix round 4.5b2, part A).
|
||||
// A < followed (continuations skipped) by an odd run of backslashes and a
|
||||
// second < is a heredoc operator a second shell may read: the outer shell
|
||||
// removes the backslashes, so eval, bash -c or sh -c reads << there. It is
|
||||
// blocked as an unquoted opener the guard could not read when the word after
|
||||
// the second < names the delimiter as a second shell reads it (every
|
||||
// backslash-newline removed, then a leading -, blanks, quotes and
|
||||
// backslashes) and a non-blank line follows anywhere after the opener's line
|
||||
// (the body may sit inside the quotes that carry the word). It is judged
|
||||
// before every skip. A one-line form has no such line and passes. The rule
|
||||
// does not look for the second shell, so the same text with none is blocked
|
||||
// as well (fix round 4.5b2, part A, decision 107).
|
||||
// The shell reading fails closed: an error in it blocks (exit 2), because an
|
||||
// exit 1 is a non-blocking error and the command would run.
|
||||
const ARGS_DELIM = 'VOYAGE_ARGS_EOF_7F3C9E';
|
||||
|
|
@ -504,9 +515,30 @@ function argsCutInString(text, o) {
|
|||
return text.slice(lineStart, o.index).split(q).length % 2 === 0;
|
||||
}
|
||||
|
||||
// After the second < of a twice-escaped operator (p), does the word a second
|
||||
// shell reads name the delimiter? Every backslash-newline is removed, then a
|
||||
// leading -, blanks, quotes and backslashes (fix round 4.5b2, part A,
|
||||
// decision 107).
|
||||
function argsRereadNames(text, p) {
|
||||
for (;;) {
|
||||
if (text[p] === '\\' && text[p + 1] === '\n') { p += 2; continue; }
|
||||
if ('- \t\'"\\'.includes(text[p]) && p < text.length) { p += 1; continue; }
|
||||
break;
|
||||
}
|
||||
let j = 0;
|
||||
while (j < ARGS_DELIM.length && p < text.length) {
|
||||
if (text[p] === '\\' && text[p + 1] === '\n') { p += 2; continue; }
|
||||
if (text[p] !== ARGS_DELIM[j]) return false;
|
||||
p += 1;
|
||||
j += 1;
|
||||
}
|
||||
return j === ARGS_DELIM.length;
|
||||
}
|
||||
|
||||
// Each heredoc operator in `text`, in text order, as the shell reads its word:
|
||||
// { index (the operator's first <), end, word, quoted, unreadable, cutOnly,
|
||||
// dash (the - of <<- was read) }. Every < it judges escaped goes into
|
||||
// dash (the - of <<- was read), reread (a twice-escaped operator a second
|
||||
// shell reads, decision 107) }. Every < it judges escaped goes into
|
||||
// lim.escaped, which the regex reading reads.
|
||||
function argsShellOpeners(text, lim = argsEscapeLimit(text)) {
|
||||
const n = text.length;
|
||||
|
|
@ -518,6 +550,14 @@ function argsShellOpeners(text, lim = argsEscapeLimit(text)) {
|
|||
const openers = [];
|
||||
let i = text.indexOf('<');
|
||||
while (i !== -1) {
|
||||
{
|
||||
const r = skip(i + 1);
|
||||
let b = 0;
|
||||
while (text[r + b] === '\\') b += 1;
|
||||
if (b % 2 === 1 && text[r + b] === '<' && argsRereadNames(text, r + b + 1)) {
|
||||
openers.push({ index: i, end: r + b + 1, word: '', quoted: false, unreadable: true, cutOnly: false, dash: false, reread: true });
|
||||
}
|
||||
}
|
||||
if (argsEscapedLt(text, i, lim, openers)) {
|
||||
lim.escaped.add(i);
|
||||
i = text.indexOf('<', i + 1);
|
||||
|
|
@ -624,6 +664,13 @@ if (command.replace(/\\\n/g, '').includes(ARGS_DELIM)) {
|
|||
}
|
||||
let bodyFrom = Infinity;
|
||||
for (const o of argsShell) {
|
||||
if (o.reread) {
|
||||
if (/\n\s*\S/.test(command.slice(o.index))) {
|
||||
argsVerdict = [true, true];
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (o.index >= bodyFrom && o.cutOnly && !argsSplit(command, o)) continue;
|
||||
if (o.cutOnly && argsCutInString(command, o)) continue;
|
||||
// A delimiter split by a backslash-newline whose word has a suffix is
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue