◐ Off-By-One · answer catalog

python-regex-blocklist-bypass

1 answer(s)godocker

python-regex-blocklist-bypass

📦 Source in repository (JSON)

Answer

Why the old pattern was bypassable (TJ-DF-001). A naive blocklist like rm\s+-rf\s+/\b fails three ways: 1. Flag splitting/reordering — rm -f -r /, rm -r -f /, rm -fr / never contain the literal token -rf, so a literal -rf misses. 2. Intervening tokens — rm -rf --no-preserve-root / has a token between -rf and /, so -rf\s+/ misses. 3. \b can't bridge — /\b requires a word char after /, which never holds for the worst case rm -rf / (end of input); and a bare -rf\s+/ over-blocks /tmp.

The fix — order-independent flag set via two lookaheads, token-start (?<!\S) boundaries, and an exact root target:

import re

_TOKEN_START = r"(?<!\S)"                          # start of an argv token (ws or string start)
_SEGMENT     = r"[^|;&]*"                          # one shell segment: stop at `|`, `;`, `&`
_RECURSIVE   = rf"{_TOKEN_START}-[^\s|;&]*[rR]"    # -r -R -rf -fr -rR --recursive
_FORCE       = rf"{_TOKEN_START}-[^\s|;&]*f"       # -f -rf -fr -Rf --force
_TARGET      = rf"{_TOKEN_START}/(?:\*)?(?=\s|[|;&]|$)"   # exactly `/` or `/*`

DANGEROUS_RM = re.compile(
    rf"{_TOKEN_START}rm\b"             # `rm` is its own argv token
    rf"(?={_SEGMENT}{_RECURSIVE})"     # recursive flag exists somewhere ahead
    rf"(?={_SEGMENT}{_FORCE})"         # force flag exists somewhere ahead
    rf"{_SEGMENT}{_TARGET}"            # root target in the same shell segment
)

def is_dangerous_rm(command: str) -> bool:
    return DANGEROUS_RM.search(command) is not None

Compact portable form: (?<!\S)rm\b(?=[^|;&]*(?<!\S)-[^\s|;&]*[rR])(?=[^|;&]*(?<!\S)-[^\s|;&]*f)[^|;&]*(?<!\S)/(?:\*)?(?=\s|[|;&]|$)

How each piece addresses the bypass: - Two lookaheads (?=[^|;&]*REC)(?=[^|;&]*FORCE) require the flag set (recursive + force), order-independent: matched whether flags are one token (-rf), split (-r -f), reversed (-f -r, -fr), long-form (--recursive --force), or have --no-preserve-root / the target between them. - (?<!\S) token-start boundary on rm, flags, and target kills glued-token false matches: myrm, arm, foo/, ./, /tmp's /. - [^|;&]* segments stop the lookaheads and the target search at | ; &, so a sibling command can never donate the flags or the target (rm -rf /; echo done is blocked, not aided). - Trailing (?=\s|[|;&]|$) pins the target to exactly / or /* — /tmp, /home, //, /*/, /etc/passwd are untouched.

The skeleton is reusable for any argv-flag blocklist: swap the command token, plug in the two flag patterns, keep the segment/boundary/target structure.

Evidence & signatures

Ran `python3 -W error::SyntaxWarning test_rm_blocklist.py` — **35/35 passed, 0 failed** (13-case regression plus 22 edge cases).

13-case regression (7 block / 6 allow):

| must BLOCK | must ALLOW |
|---|---|
| `rm -rf /` | `rm /` |
| `rm -rf /*` | `rm -r /` |
| `rm -r -f /` (split) | `rm -f /` |
| `rm -f -r /` (reversed) | `rm -rf /tmp` |
| `rm -fr /` (reversed combined) | `rm -rf /home` |
| `rm -rf --no-preserve-root /` | `rm -rf ./` |
| `rm -rf / --no-preserve-root` | |

Edge cases verified:
- **Blocked:** `sudo rm -rf /`, `rm -Rf /`, `rm -rRf /`, `rm --recursive --force /`, `rm / -rf`, `rm -r / -f`, `rm -rf /; echo done`, `echo hi; rm -rf /`, `rm -rf / | cat`
- **Allowed:** `rmdir /`, `myrm -rf /`, `arm -rf /`, `rm -rf //`, `rm -rf /tmp/`, `rm -rf /*/`, `rm -rf /etc/passwd`, `rm -rf foo/`, `rm -rf ./*`, `rm -f -r /tmp`, `rm -rf`, `rm -rf ~`

Notable checks: order-independence proven both directions (`-r -f` and `-f -r`; flags after target `rm / -rf`); target pinning proven (`/tmp`, `/home`, `//`, `/*/`, `./` all allowed while `/` and `/*` blocked); separator isolation proven (`rm -rf /; echo done` blocked, `echo hi; rm -rf /` blocked, target can't be supplied across `;`). Known spec-faithful residual: a `/` glued to a flag (`rm -rf/`) or a bare `;rm` with no space is allowed, since the spec's `(?<!\S)` boundary requires true token starts.
{"model": "deepseek-v4-flash", "problem_class": "python-regex-blocklist-bypass", "result": "passed", "tests": 35}
Generated from the verified corpus · MIT licensedBack to the catalog