fix(hooks): also read a group's writes joined in the order the shell receives them

The commands in a group write one stream into the pipe, so a bypass split across them, as in { printf 'git pu'; printf 'sh --no-verify'; } | sh, was read as two harmless fragments. groupSources() now adds the outputs joined in order next to the separate ones. The group handling moves out of pipelineSources() to keep it under the complexity limit.
This commit is contained in:
L4XB committed 2026-10-23 07:33:24 +02:00
1 parent b8ce049abb
commit fc557e57ff
2 files changed
+16 -7

No files matched your search

+14 -7
View File
@@ -815,18 +815,25 @@ function commandRole(words, budget) {
return { kind: 'opaque', stdin: true };
}
// A brace group or subshell writes what its own commands print into the pipe:
// `{ echo 'git push --no-verify'; } | sh` feeds sh like `echo ... | sh`. The
// writes form one stream, so they are also read joined in order:
// `{ printf 'git pu'; printf 'sh --no-verify'; } | sh`.
function groupSources(scope, budget) {
const written = [];
for (const inner of scanShell(scope.body, budget).commands) {
if (!inner.pipeTo) written.push(...pipelineSources(inner, budget));
}
if (written.length > 1) written.push({ text: written.map(source => source.text).join(''), opaque: written.some(source => source.opaque) });
return written;
}
// Literal producers only. Unmodeled transformations remain conservative rather
// than executing a formatter, interpreter, shell or user-supplied command.
function pipelineSources(command, budget) {
const sources = [];
for (let current = command; current; current = current.pipeFrom) {
// A brace group or subshell writes what its own commands print into the
// pipe: `{ echo 'git push --no-verify'; } | sh` feeds sh like `echo ... | sh`.
if (current.closedScope) {
for (const inner of scanShell(current.closedScope.body, budget).commands) {
if (!inner.pipeTo) sources.push(...pipelineSources(inner, budget));
}
}
if (current.closedScope) sources.push(...groupSources(current.closedScope, budget));
budget.spend(current.words.length + 1);
const { words } = executableWords(current.words, budget);
for (const word of words) budget.spend(word.value.length + 3);
+2
View File
@@ -239,6 +239,8 @@ const executingPayloads = [
['pipe subshell data to bash', "(echo 'git push --no-verify'; printf x) | bash"],
['pipe nested group data to sh', "{ (printf '%s\\n' 'git commit -n -m x'); } | sh"],
['pipe group pipeline data to sh', "{ echo 'git push --no-verify' | grep git; } | sh"],
['pipe consecutive group writes to sh', "{ printf 'git pu'; printf 'sh --no-verify'; } | sh"],
['pipe echo -n and echo group writes to bash', "{ echo -n 'git commit '; echo '--no-verify -m x'; } | bash"],
['execute data through xargs and bash -c', "printf '%s\\n' 'git commit -n -m x' | xargs -I CMD bash -c CMD"],
['execute command substitution text through bash', "echo '$(git commit -n -m x)' | bash"],
['execute bash here-string', "bash <<< 'git commit -n -m x'"],