fix: sanitize subprocess call in runner.py (#2149)

* fix: V-001 security vulnerability

Automated security fix generated by OrbisAI Security

* fix: sanitize subprocess call in runner.py

The runner

* fix: address PR review comments on V-001 allowlist and test coverage

Remove dangerous interpreters (python, python3, node, curl, wget) from
ALLOWED_SETUP_EXECUTABLES — they can execute arbitrary code via argument
flags and are not needed for sandbox setup. Rewrite test_invariant_runner
to call _setup_sandbox directly instead of spawning runner.py as a
subprocess (which had no __main__ entrypoint and never exercised the fix).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OrbisAI Security 2026-06-15 23:19:45 +05:30 committed by GitHub
parent 1c3280dc0d
commit cf59d0d283
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 72 additions and 0 deletions

View File

@ -15,6 +15,11 @@ from scripts.scenario_generator import Scenario
SANDBOX_BASE = Path("/tmp/skill-comply-sandbox")
ALLOWED_MODELS = frozenset({"haiku", "sonnet", "opus"})
ALLOWED_SETUP_EXECUTABLES = frozenset({
"git", "npm", "pip", "pip3",
"touch", "mkdir", "cp", "mv", "echo",
"chmod", "unzip", "tar",
})
# Shell builtins cannot be invoked via subprocess.run; cwd is already
# controlled by the cwd= keyword. Scenarios that include these in
# setup_commands (a common shell-style convention) must be tolerated.
@ -106,6 +111,9 @@ def _setup_sandbox(sandbox_dir: Path, scenario: Scenario) -> None:
if not parts or parts[0] in SHELL_BUILTINS:
# Shell builtins (cd/pushd/popd) cannot run as subprocess; skip.
continue
if parts[0] not in ALLOWED_SETUP_EXECUTABLES:
# Restrict to known-safe executables to prevent arbitrary code execution.
continue
try:
subprocess.run(parts, cwd=sandbox_dir, capture_output=True)
except FileNotFoundError:

View File

@ -0,0 +1,64 @@
import os
import sys
import pytest
from pathlib import Path
_SKILL_COMPLY_ROOT = Path(__file__).resolve().parent.parent / "skills" / "skill-comply"
if str(_SKILL_COMPLY_ROOT) not in sys.path:
sys.path.insert(0, str(_SKILL_COMPLY_ROOT))
from scripts.runner import _setup_sandbox # noqa: E402
from scripts.scenario_generator import Scenario # noqa: E402
_GLOBAL_MARKER = "/tmp/runner_test_pwned_marker"
@pytest.fixture(autouse=True)
def _remove_marker():
if os.path.exists(_GLOBAL_MARKER):
os.remove(_GLOBAL_MARKER)
yield
if os.path.exists(_GLOBAL_MARKER):
os.remove(_GLOBAL_MARKER)
@pytest.mark.parametrize(
"setup_commands,test_id",
[
(
("python -c \"import os; os.system('touch /tmp/runner_test_pwned_marker')\"",),
"python_interpreter",
),
(
("../../../../../../bin/sh -c 'touch /tmp/runner_test_pwned_marker'",),
"path_traversal",
),
(
("bash -c 'touch /tmp/runner_test_pwned_marker'",),
"non_allowlisted_binary",
),
(
("echo hello",),
"benign_echo",
),
],
ids=["python_interpreter", "path_traversal", "non_allowlisted_binary", "benign_echo"],
)
def test_setup_sandbox_blocks_dangerous_commands(setup_commands, test_id, tmp_path):
"""Invariant: _setup_sandbox must not execute disallowed commands."""
scenario = Scenario(
id=f"test-{test_id}",
level=1,
level_name="basic",
description="security test scenario",
prompt="",
setup_commands=setup_commands,
)
sandbox_dir = tmp_path / "sandbox"
_setup_sandbox(sandbox_dir, scenario)
assert not os.path.exists(_GLOBAL_MARKER), (
f"Arbitrary command execution detected for '{test_id}': "
f"marker file created at {_GLOBAL_MARKER}"
)