如何限制subprocess.run()执行的用户代码仅使用标准流,禁止文件读写?
不用Docker也能搞定!针对你担心的用户恶意代码读写文件问题,这里有几个轻量、易用的方案,适配树莓派的资源限制:
1. 用Bubblewrap打造完全隔离的沙箱
Bubblewrap(简称bwrap)是个超轻量的Linux沙箱工具,资源占用极低,专门用来给进程创建隔离环境,完美适配树莓派。它能彻底切断用户代码和主机文件系统的联系,只保留stdin/stdout/stderr三个IO通道。
操作步骤:
首先安装它:
sudo apt update && sudo apt install bubblewrap
然后修改你的Python执行逻辑:
原来你是直接调用python3执行用户代码,现在用bwrap把它包起来,让用户代码完全看不到主机的文件系统:
def run_cmd(args, v_stdin, timelimit=10): p = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=v_stdin, encoding='utf-8', timeout=timelimit) return p.stdout.strip(), p.stderr.strip() # 替换原来的run_cmd调用 # 注意:把输入文件的内容通过stdin传递,不再传文件路径(因为沙箱里看不到) with open('example-input.in', 'r') as f: input_content = f.read() # 构造bwrap命令:完全隔离文件系统,只保留必要的IO和Python运行依赖 sandbox_args = [ 'bwrap', '--dev-null', '/', # 把所有路径指向/dev/null,无法读写任何文件 '--bind', '/usr/lib/python3.5', '/usr/lib/python3.5', # 替换成你树莓派上的Python版本路径 '--bind', '/usr/lib/arm-linux-gnueabihf', '/usr/lib/arm-linux-gnueabihf', # 依赖的系统库 '--unshare-all', # 取消所有命名空间共享(网络、PID等) '--stdin', 'fd:0', # 保留标准输入 '--stdout', 'fd:1', # 保留标准输出 '--stderr', 'fd:2', # 保留标准错误 'python3', sys.argv[1] # 执行用户代码 ] out, err = run_cmd(sandbox_args, input_content, timelimit=int(sys.argv[4]))
这样用户代码就算写了open('/var/www/html/index.php', 'w'),也会因为找不到文件路径(所有路径都是/dev/null)而失败,彻底杜绝文件读写。
为啥选它?
- 配置简单,命令行参数就能搞定,不用复杂的配置文件
- 资源占用极低,比Docker小太多,树莓派毫无压力
- 安全性拉满,完全隔离文件系统,还能限制网络、PID等其他资源
2. 用Seccomp过滤系统调用(纯Python实现)
Seccomp是Linux内核自带的安全特性,可以精准控制进程能调用的系统函数。通过python-seccomp库,你可以在Python代码里直接禁止用户代码调用任何和文件操作相关的系统调用,比如open、creat、unlink等。
操作步骤:
先安装依赖:
sudo apt install python3-seccomp
然后修改你的run_cmd函数,给子进程加上Seccomp规则:
import seccomp def setup_sandbox(): # 创建一个过滤器,默认禁止所有系统调用 filter = seccomp.SyscallFilter(defaction=seccomp.KILL) # 只允许维持进程运行和IO的必要系统调用 allowed_calls = [ 'read', 'write', 'exit', 'exit_group', 'brk', 'mmap', 'munmap', 'mprotect', 'rt_sigreturn', 'sigaltstack' ] for call in allowed_calls: filter.add_rule(seccomp.ALLOW, call) # 加载规则,生效 filter.load() def run_cmd(args, v_stdin, timelimit=10): # 在子进程启动前应用沙箱规则 p = subprocess.run( args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, input=v_stdin, encoding='utf-8', timeout=timelimit, preexec_fn=setup_sandbox # 子进程启动时执行这个函数 ) return p.stdout.strip(), p.stderr.strip()
这样用户代码一旦尝试调用文件相关的系统调用,内核会直接终止这个进程,根本不会执行恶意操作。
优点:
- 完全在Python代码里实现,不需要额外安装命令行工具
- 精准控制,只允许必要的系统调用,资源占用几乎为0
注意:
不同Python版本可能需要调整允许的系统调用列表,如果用户代码运行报错,可以查一下缺失的系统调用,加到允许列表里。
3. 低权限用户+权限控制(最简便的应急方案)
如果上面两个方案对你来说有点复杂,你可以先通过Linux用户权限来快速限制用户代码的访问,虽然安全性不如沙箱,但能直接解决网站文件被篡改的问题。
操作步骤:
- 创建一个无权限的专用用户:
sudo useradd -m -s /bin/false sandbox
- 让www-data用户(Apache运行的用户)可以无密码切换到sandbox用户执行Python脚本:
编辑sudoers文件:
sudo visudo
添加一行:
www-data ALL=(sandbox) NOPASSWD: /usr/bin/python3 /path/to/your/verify.py
- 修改PHP的调用命令,切换到sandbox用户执行:
$cmd = "sudo -u sandbox python3 verify.py $uploadedFile $questionID $uploadedFileExtension $questionTimeLimit 2>&1";
- 设置文件权限:
- 把用户上传的代码文件放在临时目录,权限设置为只有sandbox用户可读:
chown sandbox:sandbox /path/to/uploaded/files/ chmod 700 /path/to/uploaded/files/
- 确保网站目录(/var/www/html)的权限是www-data所有,sandbox用户没有读写权限:
chown www-data:www-data /var/www/html/contest/ chmod 755 /var/www/html/contest/
这样用户代码就算想写index.php,也因为sandbox用户没有权限而失败,虽然不能完全禁止读取系统文件,但能有效保护你的网站文件。
方案推荐
- 追求最高安全性和轻量:选Bubblewrap,隔离彻底,资源占用低
- 想纯Python代码实现:选Seccomp,精准控制系统调用
- 快速解决问题,不想折腾:选低权限用户方案,配置简单
内容的提问来源于stack exchange,提问作者Lucas

