Skip to content

Commit f32edbd

Browse files
authored
fix(PythonInterpreter): Remove overly permissive read permissions and add strict allow (#9081)
1 parent 3ed69f0 commit f32edbd

File tree

2 files changed

+84
-3
lines changed

2 files changed

+84
-3
lines changed

dspy/primitives/python_interpreter.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import json
2+
import logging
23
import os
34
import subprocess
45
from os import PathLike
56
from types import TracebackType
67
from typing import Any
78

9+
logger = logging.getLogger(__name__)
810

911
class InterpreterError(RuntimeError):
1012
pass
@@ -37,8 +39,9 @@ def __init__(
3739
"""
3840
Args:
3941
deno_command: command list to launch Deno.
40-
enable_read_paths: Files or directories to allow reading from in the sandbox.
41-
enable_write_paths: Files or directories to allow writing to in the sandbox.
42+
enable_read_paths: Files or directories to allow reading from in the sandbox.
43+
enable_write_paths: Files or directories to allow writing to in the sandbox.
44+
All write paths will also be able to be read from for mounting.
4245
enable_env_vars: Environment variable names to allow in the sandbox.
4346
enable_network_access: Domains or IPs to allow network access in the sandbox.
4447
sync_files: If set, syncs changes within the sandbox back to original files after execution.
@@ -56,7 +59,22 @@ def __init__(
5659
if deno_command:
5760
self.deno_command = list(deno_command)
5861
else:
59-
args = ["deno", "run", "--allow-read"]
62+
args = ["deno", "run"]
63+
64+
# Allow reading runner.js and explicitly enabled paths
65+
allowed_read_paths = [self._get_runner_path()]
66+
67+
# Also allow reading Deno's cache directory so Pyodide can load its files
68+
deno_dir = self._get_deno_dir()
69+
if deno_dir:
70+
allowed_read_paths.append(deno_dir)
71+
72+
if self.enable_read_paths:
73+
allowed_read_paths.extend(str(p) for p in self.enable_read_paths)
74+
if self.enable_write_paths:
75+
allowed_read_paths.extend(str(p) for p in self.enable_write_paths)
76+
args.append(f"--allow-read={','.join(allowed_read_paths)}")
77+
6078
self._env_arg = ""
6179
if self.enable_env_vars:
6280
user_vars = [str(v).strip() for v in self.enable_env_vars]
@@ -77,6 +95,36 @@ def __init__(
7795
self.deno_process = None
7896
self._mounted_files = False
7997

98+
_deno_dir_cache = None
99+
100+
@classmethod
101+
def _get_deno_dir(cls) -> str | None:
102+
if cls._deno_dir_cache:
103+
return cls._deno_dir_cache
104+
105+
if "DENO_DIR" in os.environ:
106+
cls._deno_dir_cache = os.environ["DENO_DIR"]
107+
return cls._deno_dir_cache
108+
109+
try:
110+
# Attempt to find deno in path or use just "deno"
111+
# We can't easily know which 'deno' will be used if not absolute, but 'deno' is a safe bet
112+
result = subprocess.run(
113+
["deno", "info", "--json"],
114+
capture_output=True,
115+
text=True,
116+
check=False
117+
)
118+
if result.returncode == 0:
119+
info = json.loads(result.stdout)
120+
cls._deno_dir_cache = info.get("denoDir")
121+
return cls._deno_dir_cache
122+
except Exception:
123+
logger.warning("Unable to find the Deno cache dir.")
124+
pass
125+
126+
return None
127+
80128
def _get_runner_path(self) -> str:
81129
current_dir = os.path.dirname(os.path.abspath(__file__))
82130
return os.path.join(current_dir, "runner.js")

tests/primitives/test_python_interpreter.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,36 @@ def test_enable_net_flag():
169169
)
170170
result = interpreter.execute(code)
171171
assert int(result) == 200, "Network access is permitted with enable_network_access"
172+
173+
174+
def test_interpreter_security_filesystem_access(tmp_path):
175+
"""
176+
Verify that the interpreter cannot read arbitrary files from the host system
177+
unless explicitly allowed.
178+
"""
179+
# 1. Create a "secret" file on the host
180+
secret_file = tmp_path / "secret.txt"
181+
secret_content = "This is a secret content"
182+
secret_file.write_text(secret_content)
183+
secret_path_str = str(secret_file.absolute())
184+
185+
# 2. Attempt to read the file WITHOUT permission
186+
malicious_code = f"""
187+
import js
188+
try:
189+
content = js.Deno.readTextFileSync('{secret_path_str}')
190+
print(content)
191+
except Exception as e:
192+
print(f"Error: {{e}}")
193+
"""
194+
195+
with PythonInterpreter() as interpreter:
196+
output = interpreter(malicious_code)
197+
assert "Requires read access" in output
198+
assert secret_content not in output
199+
200+
# 3. Attempt to read the file WITH permission
201+
with PythonInterpreter(enable_read_paths=[secret_path_str]) as interpreter:
202+
output = interpreter(malicious_code)
203+
assert secret_content in output
204+

0 commit comments

Comments
 (0)