diff --git a/scripts/hooks/block-no-verify.js b/scripts/hooks/block-no-verify.js index 5b1cfcee2..75755260f 100644 --- a/scripts/hooks/block-no-verify.js +++ b/scripts/hooks/block-no-verify.js @@ -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); diff --git a/scripts/hooks/lib/shell-scan.js b/scripts/hooks/lib/shell-scan.js index 94a23a32a..953833485 100644 --- a/scripts/hooks/lib/shell-scan.js +++ b/scripts/hooks/lib/shell-scan.js @@ -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; diff --git a/tests/hooks/block-no-verify.test.js b/tests/hooks/block-no-verify.test.js index 2cf25a1e1..5dac62720 100644 --- a/tests/hooks/block-no-verify.test.js +++ b/tests/hooks/block-no-verify.test.js @@ -235,6 +235,10 @@ const executingPayloads = [ ['word-internal hash does not hide a later push bypass', 'printf %s foo#bar && git push --no-verify'], ['pipe echo data to bash', "echo 'git commit -n -m x' | bash"], ['pipe printf data to sh', "printf '%s\\n' 'git commit --no-verify -m x' | sh"], + ['pipe brace group data to sh', "{ echo 'git commit --no-verify -m x'; } | sh"], + ['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"], ['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'"],