fix(llm-security): HIGH — bare root/home targets bypassed the rm block
The BLOCK rule named "Filesystem root destruction" did not block the bare root target it is named for. The target alternation ended in a shared `\b`, and a word boundary cannot hold after `/` or `~` at end-of-command. Measured against the shipped pattern: bare `/` target NOT blocked `$HOME` target blocked bare `~` target NOT blocked `/usr` target blocked `/*` glob target NOT blocked swapped flags (-fr) NOT blocked sudo-prefixed NOT blocked Only targets whose first character is a word character ever satisfied the assertion, so the rule caught `/etc` but not bare root. These fell through to WARN (exit 0) — advisory only, command executed. - Pattern: `\b` moved onto the `$HOME` alternative alone, where it is meaningful (it ends in a word char, so `$HOMEDIR` is still not swallowed). Dropped from `/` and `~`. Nothing else changes: `/etc`, `/home`, `./build` behave exactly as before. - The old test file encoded this defect as expected behaviour, with a NOTE claiming the pattern "requires separate flag groups (e.g. -f -r, not -rf combined)". That diagnosis was wrong — the `/etc` case blocks fine with merged flags. Comment replaced with the real root cause. npm test: 1872/1872 green (1865 + 7 new). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
This commit is contained in:
parent
24949860f5
commit
a92b3c5962
2 changed files with 23 additions and 4 deletions
|
|
@ -21,7 +21,11 @@ import { getPolicyValue } from '../../scanners/lib/policy-loader.mjs';
|
|||
const BLOCK_RULES = [
|
||||
{
|
||||
name: 'Filesystem root destruction (rm -rf /)',
|
||||
pattern: /\brm\s+(?:-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+(?:\/|~|\$HOME)\b/,
|
||||
// The target alternation must NOT end in a shared `\b`: a trailing word-boundary
|
||||
// cannot hold after `/` or `~` at end-of-command, so the bare `rm -rf /` and
|
||||
// `rm -rf ~` forms this rule is named for would fall through. `\b` belongs only on
|
||||
// `$HOME`, which ends in a word character (so `$HOMEDIR` is not swallowed).
|
||||
pattern: /\brm\s+(?:-[a-zA-Z]*f[a-zA-Z]*\s+|--force\s+)*-[a-zA-Z]*r[a-zA-Z]*\s+(?:\/|~|\$HOME\b)/,
|
||||
description:
|
||||
'`rm -rf /`, `rm -rf ~`, and `rm -rf $HOME` would destroy the entire filesystem ' +
|
||||
'or home directory. This command is unconditionally blocked.',
|
||||
|
|
|
|||
|
|
@ -17,9 +17,24 @@ function bashPayload(command) {
|
|||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('pre-bash-destructive — BLOCK cases', () => {
|
||||
// NOTE: The block pattern requires separate flag groups (e.g. -f -r, not -rf combined).
|
||||
// `rm -rf /` with merged flags is caught only by the WARN rule, not the BLOCK rule.
|
||||
// Commands with split flags and a word-boundary target are reliably blocked.
|
||||
// Regression: the root-destruction rule previously ended in a `\b` assertion, which
|
||||
// cannot hold after a non-word target character. `rm -rf /` and `rm -rf ~` — the two
|
||||
// most literal forms the rule is named for — therefore fell through to WARN only.
|
||||
// The bare-target cases below are the regression guard; do not relax them.
|
||||
|
||||
for (const cmd of ['rm -rf /', 'rm -rf ~', 'rm -rf /*', 'rm -fr /', 'sudo rm -rf /', 'rm -rf ~/']) {
|
||||
it(`blocks ${cmd} (bare root/home target)`, async () => {
|
||||
const result = await runHook(SCRIPT, bashPayload(cmd));
|
||||
assert.equal(result.code, 2, `expected BLOCK (exit 2) for: ${cmd}`);
|
||||
assert.match(result.stderr, /BLOCKED/);
|
||||
assert.match(result.stderr, /Filesystem root destruction/);
|
||||
});
|
||||
}
|
||||
|
||||
it('does not block rm -rf $HOMEDIR (different variable, not $HOME)', async () => {
|
||||
const result = await runHook(SCRIPT, bashPayload('rm -rf $HOMEDIR/cache'));
|
||||
assert.notEqual(result.code, 2);
|
||||
});
|
||||
|
||||
it('blocks rm -f -r /home (split flags targeting root-level directory)', async () => {
|
||||
const result = await runHook(SCRIPT, bashPayload('rm -f -r /home'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue