第 6 章 · 02 安全边界:黑名单与"不是沙箱"警告 ★


第 6 章 · 02 安全边界:黑名单与"不是沙箱"警告 ★

本节摘要:上一节看到解释器「挡了什么」,本节直面它「挡不住什么」。先盘清三道防线的源码:DANGEROUS_MODULES 模块黑名单(os/subprocess/socket 等)、DANGEROUS_FUNCTIONS 函数黑名单、dunder 属性禁止(焊死 __class__/__globals__ 等逃逸路径);然后是官方 README 的警告原文——LocalPythonExecutor "is not a security sandbox",黑名单可被绕过、解释器与主进程同权限、LLM 仍可读写文件系统;接着给出决策框架:什么场景本地解释器够用(可信环境/快速原型/自己的机器),什么场景必须沙箱(生产/不可信模型/多租户);最后补上 serialization.py 的 pickle 警告——反序列化任意 agent 文件等于任意代码执行。安全分层哲学一句话:本地解释器挡意外,沙箱挡恶意。

内容来源:src/smolagents/local_python_executor.pysrc/smolagents/serialization.pyREADME.md

⚠️ 注意:本节结论务必记住:不要用 LocalPythonExecutor 执行任何你不信任的代码。黑名单是「减速带」,不是「围墙」。

学习目标

  1. 能列出 DANGEROUS_MODULES 与 DANGEROUS_FUNCTIONS 清单并说明拦截位置。
  2. 理解 dunder 禁止堵的是哪几条逃逸路径。
  3. 能复述官方"not a security sandbox"警告的两层含义。
  4. 掌握本地/沙箱的选型决策框架。
  5. 记住 pickle 反序列化的风险与 SafeSerializer 的默认防线。

一、DANGEROUS_MODULES 与 DANGEROUS_FUNCTIONS:两份黑名单

源码中的两份清单(完整贴出,值得逐个认识):

# Non-exhaustive list of dangerous modules that should not be imported DANGEROUS_MODULES = [ "builtins", "io", "multiprocessing", "os", "pathlib", "pty", "shutil", "socket", "subprocess", "sys", ] DANGEROUS_FUNCTIONS = [ "builtins.compile", "builtins.eval", "builtins.exec", "builtins.globals", "builtins.locals", "builtins.__import__", "os.popen", "os.system", "posix.system", ]

第一份注释就很诚实:「Non-exhaustive」——非穷尽清单。模块黑名单挡的是系统级能力:os(文件与进程)、subprocess(起子进程)、socket(网络)、shutil(递归删文件)、sys(解释器内部)、builtins(绕过所有函数白名单的钥匙)、pty(伪终端)、io/pathlib(文件系统)、multiprocessing(并行进程)。函数黑名单挡的是「动态执行三件套」(compile/eval/exec——拿到它们就能脱离 AST 解释器直接跑任意字符串)和「命名空间窥探」(globals/locals——可能摸到主进程的对象),以及 os.popen/os.system 这两个命令执行捷径。

DANGEROUS_FUNCTIONS 的生效位置在 check_safer_result——6.1 节 @safer_eval 装饰器的检查逻辑:

def check_safer_result(result, static_tools=None, authorized_imports=None): if isinstance(result, ModuleType): if not check_import_authorized(result.__name__, authorized_imports): raise InterpreterError(f"Forbidden access to module: {result.__name__}") elif isinstance(result, dict) and result.get("__spec__"): if not check_import_authorized(result["__name__"], authorized_imports): raise InterpreterError(f"Forbidden access to module: {result['__name__']}") elif isinstance(result, (FunctionType, BuiltinFunctionType)): for qualified_function_name in DANGEROUS_FUNCTIONS: module_name, function_name = qualified_function_name.rsplit(".", 1) if ( (static_tools is None or function_name not in static_tools) and result.__name__ == function_name and result.__module__ == module_name ): raise InterpreterError(f"Forbidden access to function: {function_name}")

每次求值的返回值都要验明正身:是模块就查授权;是函数就比对 __name__ + __module__ 是否命中黑名单。这样即使某条路径绕过直接调用点(比如把危险函数当成「值」传递),最后一道返回值检查也能兜住。

二、dunder 禁止:焊死元世界的大门

dunder(双下划线方法)是 Python 元世界的入口。smolagents 在三个位置拦截:

def nodunder_getattr(obj, name, default=None): if name.startswith("__") and name.endswith("__"): raise InterpreterError(f"Forbidden access to dunder attribute: {name}") return getattr(obj, name, default)

evaluate_attribute 里属性名首尾双下划线直接拒绝(连 getattr 都不给调);BASE_PYTHON_TOOLS 里的 getattr 被替换成上面的 nodunder_getattr;evaluate_call 里 dunder 函数除 __init__/__str__/__repr__ 外禁止调用。为什么必须焊死?这些是经典的沙箱逃逸链:

  • obj.__class__ → 拿到类对象 → __bases__/__mro__ 一路爬到 object;
  • obj.__globals__(函数对象专属)→ 直接摸到函数定义处的整个全局命名空间——如果解释器自身的函数在可见范围内,这一步就能拿到宿主进程的敏感对象;
  • type.__subclasses__() → 枚举内存中所有类,找到有危险方法的(比如某个持有 os 引用的对象);
  • __import__("os") → 绕过 import 白名单的走私通道;
  • __builtins__ → 整个内置命名空间,eval/exec 任取。

6.1 节提到 get_safe_module 给白名单模块做浅拷贝,也是同一思路的纵深:即使模块合法,也不把原模块对象交出去,防止被反向篡改后影响主进程。

三、官方警告:这不是安全沙箱

防线上物摆完了,现在看官方怎么定性。README 的 Security 一节原文:

Security is a critical consideration when working with code-executing agents. Ensure you are using one of the sandboxed execution options that provide isolation from untrusted code. **Warning:** `LocalPythonExecutor` provides best-effort mitigations only and is **not a security boundary**. Do not use it to run untrusted code.

以及正文里的定性:The built-in LocalPythonExecutor is not a security sandbox. It applies some restrictions but can be bypassed and must not be used as a security boundary.

关键词三个:best-effort(尽力而为)、not a security boundary(不是安全边界)、can be bypassed(可被绕过)。为什么绕过不可避免?两层根因:

第一层:黑名单打的是补丁,白名单才有边界。DANGEROUS_MODULES 自己承认「非穷尽」——Python 标准库和第三方包里能碰文件/网络/子进程的入口成百上千(ctypesimportlib、各种框架的 os 再导出……),黑名单永远追不全。真正硬的边界只有 authorized_imports 白名单,但用户为了实用性常常扩白名单(加 pandas、requests),每扩一个,攻击面就大一分。

第二层:解释器与主进程同权限,共享同一台机器。这是根本性的:解释器本身就是主进程里的普通 Python 代码,LLM 生成的代码一旦找到任何一个未封堵的原生对象,它拿到的权限就是你这个进程的权限——你的文件系统、你的环境变量、你的网络。官方文档点破:LLM 可以读写文件系统(比如你授权了 pathlib 或某个内部用到文件操作的库)、可以发起网络请求。没有操作系统级的隔离,进程内的任何防线理论上都可被突破。

顺带把第 4.2 节的伏笔收掉:serialization 的 pickle 警告与这里同源。SafeSerializer.loads 默认拒绝 pickle 数据、遇到 pickle 前缀直接抛 SerializationError,注释写明「Pickle deserialization can execute arbitrary code」——pickle.loads(恶意字节串) 等价于执行对方指定的任意代码。所以「从网上下载一个 agent 文件直接 load」与「exec 一段陌生代码」是同一种危险,from_hub 要显式 trust_remote_code 也是同一个道理。代码执行风险不止在「跑 agent 动作」这一处,凡「加载即执行」的机制都在此列。

四、决策框架:什么时候够用,什么时候必须沙箱

场景 本地解释器够用吗 理由
自己机器上做实验/原型 够用 威胁模型里没有「对手」,解释器挡的是 LLM 的失误
任务与模型都可信(自家模型、内部数据) 基本够用 主要风险是失控循环与误操作,三重闸门已覆盖
生产环境对外服务 必须沙箱 出错代价高,合规要求隔离
不可信/第三方模型 必须沙箱 模型可能被提示注入操纵,输出即潜在攻击载荷
多租户(多个用户共用) 必须沙箱 租户间数据与资源必须隔离
涉及敏感数据或凭据的环境 必须沙箱 同进程意味着同权限,环境变量/API key 都在触达范围

决策的本质是威胁建模:问自己「如果这段代码是恶意写的,最坏能发生什么?这个后果我承受得起吗?」本地解释器的合理定位是第一道防线——把 LLM 无意的死循环、误删变量、误调系统函数挡在日常运行之外;真正的恶意代码要交给 6.3 节的远程执行器,让隔离边界落在操作系统层(容器/微虚机),那样即使代码完全失控,损害也被封在一次性环境里。

这个分层可以总结成一张心智图:

威胁等级 防线 失效后果 ───────────────────────────────────────────────── 意外失误 → 本地解释器(黑名单/闸门) 低:报错重试即可 普通攻击 → 容器沙箱(Docker) 中:容器销毁重建 高级攻击 → 云沙箱(E2B/Modal/Blaxel) 低:一次性微虚机,秒级弃置

💡 阶梯要点:本阶建立全书最重要的安全观:进程内防线(黑名单/dunder 禁止/计数闸门)的价值在于「提高意外伤害的成本」,进程外隔离(容器/微虚机)才是「恶意代码的成本」。官方把话说死——LocalPythonExecutor 是 best-effort 而非 security boundary——这不是谦虚,是工程上的诚实。另外记住三个「加载即执行」:exec 陌生代码、pickle.loads 陌生字节、from_hub 陌生工具,危险等级完全相同。

本节要点回顾

  • DANGEROUS_MODULES 十项:os/subprocess/socket/shutil/sys/builtins/io/pathlib/pty/multiprocessing,注释自认「非穷尽」。
  • DANGEROUS_FUNCTIONS 九项:compile/eval/exec/globals/locals/import/os.popen/os.system/posix.system,由 check_safer_result 在每次求值返回时比对 __name__ + __module__ 拦截。
  • dunder 三处拦截:evaluate_attribute 属性访问、nodunder_getattr 替换版 getattr、evaluate_call 白名单(仅 init/str/repr);堵的是 classmroglobalssubclassesimportbuiltins 等逃逸链。
  • 官方定性:best-effort mitigations only,not a security boundary,can be bypassed;根因是黑名单非穷尽 + 解释器与主进程同权限。
  • 本地够用:可信环境/原型/自己机器;必须沙箱:生产/不可信模型/多租户/敏感环境。
  • pickle 警告:反序列化任意 agent 文件=任意代码执行;SafeSerializer.loads 默认拒绝 pickle,from_hub 需 trust_remote_code。
  • 分层哲学:本地解释器挡意外,容器挡普通攻击,云沙箱挡高级攻击。

下一节:03 四种远程沙箱执行器——RemotePythonExecutor 基类与 E2B/Docker/Modal/Blaxel 四个实现,executor_type 一参数切换,以及安全与开销的权衡。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U