From 336e4db1b88b6bb1c70c3dab0a1ba2e929e3261b Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Thu, 30 Apr 2026 15:15:29 +0200 Subject: [PATCH] =?UTF-8?q?feat(pre-bash-destructive):=20T8=20=E2=80=94=20?= =?UTF-8?q?base64-pipe-shell=20idiom=20(E9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds BLOCK_RULE for the malware-loader pattern: echo|cat|printf | base64 -d | 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. --- .../hooks/scripts/pre-bash-destructive.mjs | 11 +++++++ .../tests/hooks/pre-bash-destructive.test.mjs | 32 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/plugins/llm-security/hooks/scripts/pre-bash-destructive.mjs b/plugins/llm-security/hooks/scripts/pre-bash-destructive.mjs index 423ab4c..5abb619 100644 --- a/plugins/llm-security/hooks/scripts/pre-bash-destructive.mjs +++ b/plugins/llm-security/hooks/scripts/pre-bash-destructive.mjs @@ -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 -d | + // 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', diff --git a/plugins/llm-security/tests/hooks/pre-bash-destructive.test.mjs b/plugins/llm-security/tests/hooks/pre-bash-destructive.test.mjs index 2a74e92..b0d26a3 100644 --- a/plugins/llm-security/tests/hooks/pre-bash-destructive.test.mjs +++ b/plugins/llm-security/tests/hooks/pre-bash-destructive.test.mjs @@ -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); + }); }); // ---------------------------------------------------------------------------