diff --git a/.jules/sentinel.md b/.jules/sentinel.md index c46ef11..c8a2899 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -4,3 +4,8 @@ **Vulnerability:** Telemetry HTTP endpoints (`/status`, `/`) were completely unprotected, allowing any local user to view training state, usage, and costs. **Learning:** Initial implementation prioritized ease of use and local-only binding (`127.0.0.1`) but neglected defense-in-depth requirements for multi-user or shared environments. **Prevention:** Always implement at least Basic Authentication for any endpoint exposing state or metadata, even if restricted to loopback. Use random session-specific credentials if no configuration is provided. + +## 2025-01-24 - Environment Variable Leakage in Unit Test Gate +**Vulnerability:** `scripts/03_unit_test_gate.py` executed generated code using `subprocess.run` without filtering environment variables, allowing potentially malicious generated code to exfiltrate sensitive data (e.g., `OPENAI_API_KEY`) from the environment. +**Learning:** `subprocess.run` in Python inherits the full parent environment by default. Isolation of the working directory is insufficient if the environment remains exposed. +**Prevention:** Explicitly filter the `env` argument in `subprocess.run` using an allowlist of safe variables (e.g., `PATH`, `PYTHONPATH`, `LANG`) when executing untrusted or generated code. diff --git a/heidi_engine/telemetry.py b/heidi_engine/telemetry.py index bb89122..d14e60c 100644 --- a/heidi_engine/telemetry.py +++ b/heidi_engine/telemetry.py @@ -732,10 +732,6 @@ def get_state(run_id: Optional[str] = None) -> Dict[str, Any]: "usage": get_default_usage(), } - # BOLT OPTIMIZATION: Check thread-safe state cache - cached = _state_cache.get(target_run_id, state_file) - if cached: - return cached try: with open(state_file) as f: diff --git a/scripts/03_unit_test_gate.py b/scripts/03_unit_test_gate.py index 7507b7e..83c07e5 100755 --- a/scripts/03_unit_test_gate.py +++ b/scripts/03_unit_test_gate.py @@ -67,12 +67,15 @@ # TUNABLE: Add more dangerous patterns to block DANGEROUS_PATTERNS = [ # Dangerous imports (including comma-separated and aliased) - r"\bimport\s+[^#\n]*\b(os|subprocess|sys|shutil|socket|requests|urllib|pathlib|pickle|pty|code|bdb|pdb|multiprocessing|threading|tempfile|ftplib|smtplib|telnetlib|http|xmlrpc)\b", - r"\bfrom\s+(os|subprocess|sys|shutil|socket|requests|urllib|pathlib|pickle|pty|code|bdb|pdb|multiprocessing|threading|tempfile|ftplib|smtplib|telnetlib|http|xmlrpc)\b", + r"\bimport\s+[^#\n]*\b(os|posix|nt|subprocess|sys|shutil|socket|requests|urllib|pathlib|pickle|pty|code|bdb|pdb|multiprocessing|threading|tempfile|ftplib|smtplib|telnetlib|http|xmlrpc)\b", + r"\bfrom\s+(os|posix|nt|subprocess|sys|shutil|socket|requests|urllib|pathlib|pickle|pty|code|bdb|pdb|multiprocessing|threading|tempfile|ftplib|smtplib|telnetlib|http|xmlrpc)\b", # Dangerous built-ins r"\beval\s*\(", r"\bexec\s*\(", r"\b__import__\s*\(", + r"\b__subclasses__\b", + r"\b__globals__\b", + r"\b__builtins__\b", r"\bgetattr\s*\(", r"\bsetattr\s*\(", r"\bbreakpoint\s*\(", @@ -214,6 +217,8 @@ def test_python_code(code: str, temp_dir: str, execution_timeout: int = 5) -> Tu test_file = os.path.join(temp_dir, "test_code.py") # Wrap code to capture output safely + import textwrap + indented_code = textwrap.indent(code, " ") wrapped_code = f""" import sys import io @@ -229,7 +234,7 @@ def test_python_code(code: str, temp_dir: str, execution_timeout: int = 5) -> Tu sys.stderr = stderr_capture # Execute the user's code -{code} +{indented_code} sys.stdout = original_stdout sys.stderr = original_stderr @@ -257,13 +262,25 @@ def test_python_code(code: str, temp_dir: str, execution_timeout: int = 5) -> Tu # Try to execute with timeout try: + # SECURITY: Restrict environment variables to prevent leakage of sensitive + # information (like API keys) to the code being tested. + safe_env_keys = {"PATH", "PYTHONPATH", "LANG", "PYTHONIOENCODING"} + restricted_env = {k: v for k, v in os.environ.items() if k in safe_env_keys} + + # Ensure PYTHONPATH includes the temp directory + current_pp = restricted_env.get("PYTHONPATH", "") + if current_pp: + restricted_env["PYTHONPATH"] = f"{temp_dir}{os.pathsep}{current_pp}" + else: + restricted_env["PYTHONPATH"] = temp_dir + result = subprocess.run( [sys.executable, test_file], capture_output=True, text=True, timeout=execution_timeout, cwd=temp_dir, - env={**os.environ, "PYTHONPATH": temp_dir}, + env=restricted_env, ) stdout = result.stdout @@ -367,7 +384,9 @@ def load_jsonl(path: str) -> List[Dict[str, Any]]: def save_jsonl(samples: List[Dict[str, Any]], path: str) -> None: """Save samples to JSONL file.""" - os.makedirs(os.path.dirname(path), exist_ok=True) + dirname = os.path.dirname(path) + if dirname: + os.makedirs(dirname, exist_ok=True) with open(path, "w") as f: for sample in samples: