fix(hooks): block a bypass that a brace group or subshell pipes into a shell

pipelineSources() read the literal output of the commands that feed a shell, but a brace group or subshell producer has no words of its own, so nothing it printed reached the check. { echo 'git commit --no-verify -m x'; } | sh and (echo 'git push --no-verify'; printf x) | bash passed the guard, while echo 'git commit --no-verify -m x' | sh is blocked. shell-scan now keeps the source text of a closed scope, and pipelineSources() reads the commands in it that write to the group's output, as it already does for a plain producer.
This commit is contained in:
L4XB committed 2026-10-23 07:19:32 +02:00
1 parent 90606c8315
commit b8ce049abb
3 files changed
+15 -1

No files matched your search

+7
View File
@@ -820,6 +820,13 @@ function commandRole(words, budget) {
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));
}
}
budget.spend(current.words.length + 1);
const { words } = executableWords(current.words, budget);
for (const word of words) budget.spend(word.value.length + 3);
+4 -1
View File
@@ -449,12 +449,15 @@ function markFunctionDefinition(scan) {
}
function openScope(scan, c, incomingPipe) {
scan.scope = { parent: scan.scope, isolated: c === '(' || incomingPipe, conditional: false, pipelineLast: c === '{' && incomingPipe };
scan.scope = { parent: scan.scope, isolated: c === '(' || incomingPipe, conditional: false, pipelineLast: c === '{' && incomingPipe, bodyStart: scan.i + 1 };
scan.current.scope = scan.scope;
}
function closeScope(scan) {
const closedScope = scan.scope;
// The source text between the braces or parentheses, for a reader of the
// group's output.
closedScope.body = scan.input.slice(closedScope.bodyStart, scan.i);
scan.scope = closedScope.parent;
scan.current.scope = scan.scope;
scan.current.closedScope = closedScope;