mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-10 10:06:00 +00:00
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:
3 files changed
+15
-1
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'"],
|
||||
|
||||
Reference in new issue
Block a user