Run Commands With Argument Arrays Not Shell Strings

A command runner that executes binaries with an argument array and no shell, so untrusted values can never be interpreted as shell syntax.

PythonSecurity Boundaries

Code

import subprocess
from dataclasses import dataclass
from typing import Mapping, Optional, Sequence


class UnsafeCommandError(ValueError):
    """Raised when a command would require shell interpretation."""


@dataclass(frozen=True)
class CommandResult:
    argv: Sequence[str]
    returncode: int
    stdout: str
    stderr: str


def run_command(
    binary: str,
    args: Sequence[str],
    *,
    timeout_s: float = 10.0,
    env: Optional[Mapping[str, str]] = None,
    use_shell: bool = False,
) -> CommandResult:
    """Run binary with an argv list and no shell; return a structured result."""
    if use_shell:
        raise UnsafeCommandError("shell mode is refused; pass an argv list instead")
    if not binary:
        raise ValueError("binary must be a non-empty path")
    argv = [binary, *[str(a) for a in args]]
    try:
        completed = subprocess.run(
            argv,
            shell=False,
            capture_output=True,
            text=True,
            timeout=timeout_s,
            env=dict(env) if env is not None else None,
        )
    except subprocess.TimeoutExpired as exc:
        raise TimeoutError("command timed out after %ss" % timeout_s) from exc
    except FileNotFoundError as exc:
        raise FileNotFoundError("binary not found: %s" % binary) from exc
    return CommandResult(
        argv=argv,
        returncode=completed.returncode,
        stdout=completed.stdout,
        stderr=completed.stderr,
    )