feat(pre-bash-destructive): T8 — base64-pipe-shell idiom (E9)

Adds BLOCK_RULE for the malware-loader pattern:
  echo|cat|printf <base64-blob> | base64 -d | <shell>

This is a common RCE delivery shape that bypasses static name-matching
gates by encoding the destructive command as a base64 blob. The new
rule fires only when the final pipe target is a shell interpreter
(bash, sh, zsh, dash, ksh) — base64 decoded into jq or any non-shell
consumer remains allowed.

5 new tests in pre-bash-destructive.test.mjs:
- 3 BLOCK cases (echo|base64|bash, printf|base64|sh, cat|base64|zsh)
- 2 FP probes (base64 -d -> jq passes; base64 -d alone passes)

Closes E9 in critical-review-2026-04-20.md.
This commit is contained in:
Kjell Tore Guttormsen 2026-04-30 15:15:29 +02:00
commit 336e4db1b8
2 changed files with 43 additions and 0 deletions

View file

@ -79,6 +79,17 @@ const BLOCK_RULES = [
'strings, which is a common code injection vector. Blocked. ' +
'Refactor to use explicit commands instead.',
},
{
name: 'T8 — base64-pipe-shell idiom (echo BLOB | base64 -d | sh)',
// Matches: echo|cat|printf <base64-blob> | base64 -d | <shell>
// Common malware loader pattern that bypasses static name-matching by
// delivering the destructive command as encoded text.
pattern: /\b(?:echo|cat|printf)\s+[A-Za-z0-9+/=]+\s*\|\s*base64\s+-d\s*\|\s*(?:bash|sh|zsh|dash|ksh)\b/i,
description:
'Decoding a base64 blob and piping it directly into a shell interpreter ' +
'is a remote-code-execution loader pattern. Decode the blob first, ' +
'inspect it, then execute explicitly if safe.',
},
// Policy-defined additional blocked patterns
...getPolicyValue('destructive', 'additional_blocked', []).map(entry => ({
name: entry.name || 'Custom blocked pattern',

View file

@ -82,6 +82,38 @@ describe('pre-bash-destructive — BLOCK cases', () => {
assert.match(result.stderr, /BLOCKED/);
assert.match(result.stderr, /eval/i);
});
it('blocks T8 — base64 blob piped into bash', async () => {
const result = await runHook(SCRIPT, bashPayload('echo aGVsbG8K | base64 -d | bash'));
assert.equal(result.code, 2);
assert.match(result.stderr, /BLOCKED/);
assert.match(result.stderr, /base64/i);
});
it('blocks T8 — printf base64 blob piped into sh (no spaces around pipes)', async () => {
const result = await runHook(SCRIPT, bashPayload('printf foo|base64 -d|sh'));
assert.equal(result.code, 2);
assert.match(result.stderr, /BLOCKED/);
assert.match(result.stderr, /base64/i);
});
it('blocks T8 — cat blob piped into zsh', async () => {
const result = await runHook(SCRIPT, bashPayload('cat YWJjZGVm | base64 -d | zsh'));
assert.equal(result.code, 2);
assert.match(result.stderr, /BLOCKED/);
});
it('T8 FP probe — base64 -d to jq is NOT blocked (no shell terminator)', async () => {
// The pattern requires the final pipe target to be a shell interpreter.
// Decoding base64 to feed a JSON parser is a legitimate workflow.
const result = await runHook(SCRIPT, bashPayload('echo aGVsbG8K | base64 -d | jq .'));
assert.equal(result.code, 0);
});
it('T8 FP probe — base64 -d alone (no shell pipe) is NOT blocked', async () => {
const result = await runHook(SCRIPT, bashPayload('echo aGVsbG8K | base64 -d'));
assert.equal(result.code, 0);
});
});
// ---------------------------------------------------------------------------