Files
wehub-resource-sync fed8b2eed7
Backend release / release (push) Waiting to run
Bandit Security Scan / bandit_scan (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push multi-arch DocsGPT Docker image / manifest (push) Blocked by required conditions
Build and push DocsGPT FE Docker image for development / build (linux/amd64, ubuntu-latest, amd64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / build (linux/arm64, ubuntu-24.04-arm, arm64) (push) Waiting to run
Build and push DocsGPT FE Docker image for development / manifest (push) Blocked by required conditions
Python linting / ruff (push) Waiting to run
Run python tests with pytest / Run tests and count coverage (3.12) (push) Waiting to run
React Widget Build / build (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:28:29 +08:00

173 lines
5.5 KiB
Python

"""Hard denylist — always triggers a forced prompt, even under ``never``."""
from __future__ import annotations
import re
import shlex
from typing import List, Optional, Tuple
from application.devices.splitter import split_command, strip_wrappers
# Label/reason for the rm-root wipe, shared by the regex rule and the
# token-based helper so both report identically.
_RM_ROOT_LABEL = "rm -rf /"
# Filesystem-root targets that, with recursive+force, mean a full wipe.
_RM_ROOT_TARGETS = frozenset({"/", "/*"})
# Each entry: (label, compiled regex). Regex is matched against each
# segment's whole text (case-insensitive, after whitespace normalization).
_PATTERNS: List[Tuple[str, re.Pattern]] = [
(
_RM_ROOT_LABEL,
# ``rm`` with recursive+force targeting the filesystem root. Flags
# (incl. ``--no-preserve-root``) may appear in any order before or
# after the recursive/force flags; the target must be exactly ``/``
# or ``/*`` so safe paths (``/tmp/foo``, ``./build``, ``/home/x``)
# don't match. ``(?:-\S+\s+)*`` absorbs any extra leading options.
re.compile(
r"\brm\s+(?:-\S+\s+)*"
r"(?:-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r"
r"|--recursive\s+(?:-\S+\s+)*--force|--force\s+(?:-\S+\s+)*--recursive)"
r"\s+(?:-\S+\s+)*/\*?\s*(?:$|\s)"
),
),
(
"rm -rf ~",
re.compile(
r"\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r|-rf|-fr)\s+~"
),
),
(
"rm -rf $HOME",
re.compile(
r"\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r|-rf|-fr)\s+\$HOME\b"
),
),
(
"fork bomb",
re.compile(r":\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:"),
),
(
"dd to block device",
re.compile(
r"\bdd\s+.*\bif=/dev/(zero|random|urandom).*\bof=/dev/(sd|nvme|hd|disk|mmcblk)",
re.DOTALL,
),
),
(
"mkfs",
re.compile(r"\bmkfs(\.[a-zA-Z0-9]+)?\b"),
),
(
"shutdown",
re.compile(r"\bshutdown\b"),
),
(
"halt",
re.compile(r"\bhalt\b"),
),
(
"poweroff",
re.compile(r"\bpoweroff\b"),
),
(
"init 0/6",
re.compile(r"\binit\s+(0|6)\b"),
),
(
"git push --force",
# ``--force-with-lease`` is OK; treat ``-f`` short form as denied.
re.compile(
r"\bgit\s+push\b.*(--force(?!-with-lease)|--mirror|\s-f(?:\s|$))"
),
),
]
def _is_rm_root_wipe(segment: str) -> bool:
"""True if ``segment`` is ``rm`` with recursive+force targeting root.
Token-based so separated flags (``rm -r -f /``, ``rm -f -r /``) and
bundles (``-rf``, ``-fr``, ``-rfv``) are all caught regardless of order.
Stays conservative: requires BOTH recursive and force AND a root target
(``/`` or ``/*``), so safe paths (``/tmp/foo``, ``./build``) and
single-flag forms (``rm -r /tmp``, ``rm -f /etc/x``) are not denied.
Args:
segment: A single shell command segment (no compound connectors).
Returns:
``True`` if the segment is a recursive+force wipe of filesystem root.
"""
try:
tokens = shlex.split(segment, posix=True)
except ValueError:
# Unbalanced quotes — let the regex rules handle it; don't crash.
return False
if not tokens or tokens[0] != "rm":
return False
recursive = False
force = False
targets: List[str] = []
for tok in tokens[1:]:
if tok in ("--recursive", "-R"):
recursive = True
elif tok == "--force":
force = True
elif tok == "--no-preserve-root":
# A flag, not a target; ignore for target detection.
continue
elif tok.startswith("--"):
# Unknown long option — neither recursive/force nor a target.
continue
elif tok.startswith("-") and len(tok) > 1:
# Short flag bundle, e.g. ``-rf``, ``-fr``, ``-rfv``, ``-r``, ``-f``.
letters = tok[1:]
if "r" in letters:
recursive = True
if "f" in letters:
force = True
else:
targets.append(tok)
if not (recursive and force):
return False
return any(t in _RM_ROOT_TARGETS for t in targets)
def check_denylist(command: str) -> Optional[str]:
"""Return the matched pattern label if ``command`` hits the hard denylist.
Splits the command into segments first (see ``splitter.py``) so
``echo safe && rm -rf /`` still trips. Returns ``None`` if no segment
matches.
Args:
command: Raw shell command string.
Returns:
The human-readable pattern label, or ``None`` if no match.
"""
if not command:
return None
for segment in split_command(command):
# ``split_command`` keeps wrappers (``timeout 5 rm -rf /``,
# ``nohup rm -rf /``); strip them so the token-based rm check sees
# the real inner command and can't be evaded by a wrapper.
if _is_rm_root_wipe(strip_wrappers(segment)) or _is_rm_root_wipe(segment):
return _RM_ROOT_LABEL
for label, pattern in _PATTERNS:
if pattern.search(segment):
return label
# Also try matching the whole command as a single string, so a
# multi-line / unusual-whitespace fork bomb that splits weirdly still
# gets caught.
for label, pattern in _PATTERNS:
if pattern.search(command):
return label
return None