Python沙箱如何安全运行代码

wen 网络安全 2

本文目录导读:

Python沙箱如何安全运行代码

  1. 基础限制:exec + 受限命名空间(不安全
  2. 中级保护:eval + AST限制(适用于简单表达式)
  3. 严格限制:RestrictedPython(推荐用于简单场景)
  4. 进程级隔离:subprocess + 资源限制(推荐)
  5. 容器级隔离:Docker(最安全)
  6. 专业方案:PyPy的沙箱(已停止维护,但理念值得参考)
  7. 安全建议
  8. 实际工程选择

在Python中安全运行沙箱代码是一个复杂的课题,因为Python本身的设计目标是灵活和动态,而不是安全隔离,没有任何方案能做到100%安全,但可以根据需求选择不同级别的防护措施。

以下是几种常见的Python沙箱方案,按安全等级从低到高排列:

基础限制:exec + 受限命名空间(不安全

def exec_in_sandbox(code, allowed_builtins=None):
    # 创建一个受限的全局命名空间
    restricted_globals = {
        "__builtins__": allowed_builtins or {},
        "__name__": "__sandbox__"
    }
    try:
        exec(code, restricted_globals)
        return restricted_globals
    except Exception as e:
        return {"error": str(e)}

问题exec本身不是安全边界,很容易通过各种方式逃逸:

# 可以轻松绕过
code = """
().__class__.__bases__[0].__subclasses__()
"""

中级保护:eval + AST限制(适用于简单表达式)

import ast
def safe_eval(expr, allowed_names={}):
    # 只允许安全的AST节点
    SAFE_NODES = {
        ast.Expression, ast.Str, ast.Num, ast.Name, ast.Attribute,
        ast.Load, ast.BinOp, ast.UnaryOp, ast.Add, ast.Sub,
        ast.Mult, ast.Div, ast.Mod, ast.Pow, ast.Constant
    }
    try:
        tree = ast.parse(expr, mode='eval')
        for node in ast.walk(tree):
            if type(node) in SAFE_NODES:
                continue
            raise ValueError(f"Unsafe expression: {type(node)}")
        return eval(compile(tree, '<string>', 'eval'), 
                   {"__builtins__": {}}, allowed_names)
    except Exception as e:
        raise ValueError(f"Evaluation error: {e}")

适用范围:只能用于非常简单的数学表达式或属性访问。

严格限制:RestrictedPython(推荐用于简单场景)

from RestrictedPython import compile_restricted, safe_globals
from RestrictedPython.Guards import guarded_iter_unpack_sequence
def restricted_exec(code, local_vars=None):
    try:
        compiled_code = compile_restricted(code, '<string>', 'exec')
        restricted_globals = {
            '__builtins__': {
                'True': True,
                'False': False,
                'None': None,
                'int': int,
                'float': float,
                'str': str,
                'bool': bool,
                'list': list,
                'dict': dict,
                'tuple': tuple,
                'len': len,
                'range': range,
                'min': min,
                'max': max,
                'abs': abs,
                'print': print,  # 可选的输出函数
            },
            '_getattr_': safe_getattr,
            '_write_': guarded_write,
            '_print_': guarded_print,
        }
        restricted_locals = local_vars or {}
        exec(compiled_code, restricted_globals, restricted_locals)
        return restricted_locals
    except Exception as e:
        return {"error": str(e)}

限制:不能使用import、文件操作、网络访问等。

进程级隔离:subprocess + 资源限制(推荐)

import subprocess
import resource
import os
import signal
def run_sandboxed_code(code, timeout=5, memory_limit=100*1024*1024):
    # 将代码写入临时文件或通过管道传递
    script = f"""
import sys
import resource
# 设置资源限制
def set_limits():
    # CPU时间限制
    resource.setrlimit(resource.RLIMIT_CPU, (1, 1))
    # 内存限制
    resource.setrlimit(resource.RLIMIT_AS, ({memory_limit}, {memory_limit}))
    # 文件大小限制
    resource.setrlimit(resource.RLIMIT_FSIZE, (1024, 1024))
    # 进程数限制
    resource.setrlimit(resource.RLIMIT_NPROC, (10, 10))
set_limits()
# 执行用户代码
try:
    exec({repr(code)})
except Exception as e:
    print(f"Error: {{e}}", file=sys.stderr)
    sys.exit(1)
"""
    try:
        # 在新进程中运行
        result = subprocess.run(
            ['python3', '-c', script],
            capture_output=True,
            timeout=timeout,
            text=True
        )
        return {
            "stdout": result.stdout,
            "stderr": result.stderr,
            "returncode": result.returncode
        }
    except subprocess.TimeoutExpired:
        return {"error": "Timeout"}
    except Exception as e:
        return {"error": str(e)}

优点

  • 进程级别的隔离
  • 可以通过资源限制防止DoS攻击
  • 用户代码无法影响主程序

容器级隔离:Docker(最安全)

import docker
import tempfile
import os
def run_in_docker(code, image="python:3.9-slim", timeout=30):
    client = docker.from_env()
    # 创建临时文件存放代码
    with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
        f.write(code)
        temp_path = f.name
    try:
        # 运行容器
        container = client.containers.run(
            image=image,
            command=f"python /code/script.py",
            volumes={temp_path: {'bind': '/code/script.py', 'mode': 'ro'}},
            mem_limit="100m",
            cpu_quota=50000,  # 限制CPU使用
            network_mode="none",  # 禁用网络
            read_only=True,  # 只读文件系统
            remove=True,
            detach=True
        )
        # 等待完成
        result = container.wait(timeout=timeout)
        logs = container.logs(stdout=True, stderr=True, tail=50)
        return {
            "exit_code": result['StatusCode'],
            "output": logs.decode('utf-8')
        }
    except docker.errors.ContainerError as e:
        return {"error": str(e)}
    finally:
        os.unlink(temp_path)

专业方案:PyPy的沙箱(已停止维护,但理念值得参考)

PyPy曾有一个沙箱实现,使用独立的子解释器运行代码:

# 概念代码(需要PyPy沙箱支持)
from pypy_sandbox import Sandbox
sandbox = Sandbox()
sandbox.run("print('Hello from sandbox')")

安全建议

  1. 明确需求:确定需要开放哪些功能(算术运算、字符串处理、文件访问等)
  2. 最小权限原则:只开放必需的功能
  3. 多层防御:组合使用多种限制方法
  4. 输入验证:预处理用户输入,过滤危险字符
  5. 超时机制:设置代码执行时间限制
  6. 资源限制:内存、CPU、文件大小等

实际工程选择

  • 简单表达式求值ast.literal_evalRestrictedPython
  • 运行用户代码subprocess + 资源限制(推荐)
  • 需要网络/文件访问:Docker容器(最安全)
  • 大规模生产环境:云厂商提供的Serverless服务(AWS Lambda, Cloud Functions等)

记住:没有任何纯Python的沙箱方案是绝对安全的,对于高安全性需求,始终使用操作系统级别的隔离(容器、虚拟机)或专业的沙箱服务。

抱歉,评论功能暂时关闭!