-
Notifications
You must be signed in to change notification settings - Fork 0
THREAT MODEL
面向 AI Agent 与外部输入的 Python 代码,编译期安全转译的威胁模型。 原则:诚实披露边界——能保证什么、不能保证什么、为什么,全部写清。
| 资产 | 保护目标 |
|---|---|
| Agent 宿主进程 | 不被注入代码 / 不被任意命令执行 / 不被耗死 |
| 宿主数据 | 不被读取外泄(密钥、用户数据、内部状态) |
| 外部系统 | 不被代理 SSRF/命令执行攻击 |
| 产物可用性 | 通过编译的产物不崩溃(动态错误统一兜底) |
信任边界:源码与产物之间是编译期信任(源码中的风险代码进不了产物); 第三方模块内部是供应链信任(超出转译器能力,按 pip 生态惯例处理)。
| 关键字 | 编译期检查(checker) | 运行时注入(gen) | 拦截时机 |
|---|---|---|---|
safe |
污点数据流:input()/request.*/os.environ/safe 参数 → 赋值/容器/属性/参数传播 → 危险汇点(pickle.loads、eval/exec/compile、os.system、subprocess.*、SQL execute);int()/float()/bool() 清洗即放行;import 别名溯源(pickle as p、from pickle import loads as l) |
无(纯编译期) | 编译失败 |
only |
白名单块:访问黑名单名称(onlyDeny 33 项 ∪ escapeModules 90 项,含 builtins/pickle/marshal/os/subprocess/eval/gc/inspect/io)编译报错;块内 import 逐项过 escape 名单 |
__builtins__ = _FlyOnly(mods) 白名单代理(运行时兜底);_FlyOnly.__getattr__("__import__") 恒返回受限 _fly_sb_import
|
编译失败 + 运行时兜底 |
lock |
常量再赋值/删除、globals()['X']/vars()['X']/setattr 反射读写锁定名 |
无 | 编译失败 |
mask |
敏感数据流入输出上下文(print/logging/f-string 参数) | 无 | 编译失败 |
cage |
无(纯 gen+runtime) |
@_fly_cage(max_time, max_memory):SIGALRM + RLIMIT_AS |
运行时 |
guard |
函数签名/参数注解一致性检查 | 函数入口断言,失败抛 GuardError(带行列号) | 编译失败 + 运行时 |
seal |
seal 类实例直接属性赋值/删除拦截 |
__setattr__/__delattr__ 重写(初始化令牌放行) |
编译失败 + 运行时 |
trace |
保留前缀 _fly_ 冲突、非法级别 |
函数改名 + logging 进出日志 | 编译失败 + 运行时 |
设计原则:safe/mask/lock 是纯编译期(零运行时残留——产物里没有兜底代码,
攻击者改产物无法恢复被删掉的检查);only/seal/trace/cage/guard 是编译期检查 +
运行时注入(兜底存在于产物本身,改产物会破坏功能但兜底随之消失或被改写)。
源点 S: input() / request.* / os.environ / safe 声明的参数
传播 P: 赋值 → 容器(list/dict/set) → 属性 → 下标 → f-string → 函数参数/返回值
清洗 C: int()/float()/bool() 调用后污点清除(显式净化)
汇点 K: pickle.loads/load/Unpickler, eval/exec/compile,
os.system/popen/spawn*, subprocess.*, SQL execute, 输出上下文(print/logging)
规则(按保守序):
-
R1 直连:
t ∈ Taint(S) ∧ t 作为实参传入 K→ 报错 -
R2 传播:
x = t; x ∈ Taint(S);x ∈ Taint(S) → x[expr] ∈ Taint(S);x ∈ Taint(S) → x.attr ∈ Taint(S);f"{t}" ∈ Taint(S) -
R3 别名溯源:
import pickle as p → p 的模块属性溯源到 pickle;from pickle import loads as l → l 溯源到 pickle.loads -
R4 清洗:
t' = int(t) → Taint(t') = ∅(显式清洗 = 开发者承诺) -
R5 非路径敏感:分支内赋值即污染(
if c: x = t; K(x)报错——偏保守)
形式化定位:污点系统是**类型流敏感(flow-sensitive)+ 上下文敏感(通过 参数-返回签名建模)**的保守近似,无路径敏感(R5)。误报偏安全侧,漏报只可能 出现在 §6 列出的边界。
外部输入直接 pickle.loads/load/Unpickler → __reduce__ 任意代码执行。
防御:safe 污点汇点拦截(含别名/容器传播)。演示见
examples/cve-pickle/(5 幕:编译期 + 沙箱双层)。剩余风险:B1 跨文件、
B7 间接引用。
防御:污点汇点含 eval/exec/compile。
防御:污点汇点含 os.system/popen/spawn*/subprocess;only 白名单可封禁。
防御:mask 污点 + 输出上下文拦截。剩余风险:写文件/网络发送不属 静态输出上下文,用 only 白名单封禁(B4)。
防御:cage 函数级 max_time/max_memory(SIGALRM + RLIMIT_AS)。
防御:guard 运行时断言 + 显式清洗。
防御:lock 编译期反射拦截;seal 运行时令牌拦截。
以下全部经 fly check 实测(go test ./... 反例测试锁定):
| 对抗方式 | 结果 | 测试 |
|---|---|---|
from pickle import loads as l; l(x) 别名 |
✅ 拦截 | errors/pickle_rce.fly |
import pickle as p; p.loads(x) |
✅ 拦截 | 同上 |
__reduce__ 改换成其他 callable |
✅ 拦截(汇点是 loads,与载荷无关) | 同上 |
| 把 x 放进 list/dict 再取出 | ✅ 拦截(容器传播) | b3 实测 |
int(x) 清洗后再用 |
✅ 放行(显式清洗语义正确) | taint.go R4 |
eval("x") 动态拼接 |
✅ 拦截 | errors/safe_eval.fly |
c.x = 污点; eval(c.x) 属性赋值传播 |
✅ 拦截(属性名级精确跟踪) | redteam_test.go 属性赋值传播 |
obj = pickle.loads; obj(x)(间接引用) |
✅ 不可达(import pickle 本身 E0066) |
redteam_test.go 间接引用 |
getattr(pickle, "loads")(x) |
✅ 拦截(getattr 在危险内建名单) | redteam_test.go getattr |
globals()['x'] 动态读取后投递 |
lock_globals.fly |
注:表内 ✅ 行均有自动化回归锁定(
internal/checker/redteam_test.go表驱动 + testdata 反例), 行为漂移会直接导致测试失败。原 B7 间接引用与 B4 getattr 动态反射两项,已因pickle导入拦截(E0066)与getattr加入危险内建名单而不可达/被拦截(2026-08 实测)。
-
B1 跨文件污点:单文件编译模型,
import的其他 .fly 文件不参与污点分析。 缓解:only白名单 + 各文件独立 check。 -
B2 通配导入:
from x import *引入的名字未定义检查豁免。 - B3 第三方模块内部:模块自身代码是供应链信任,不静态审查。
-
B4 动态反射:
getattr(obj, 动态字符串)、globals()[name]的动态形式。 缓解:only白名单封禁 + 人工评审。 - B5 业务逻辑:鉴权、权限、加密等业务语义不在安全关键字职责内。
- B6 运行时兜底是"统一报错":不阻止逻辑错误本身,只保证错误被捕获并带行列号。
-
B7 间接引用:
—— 2026-08 起 不可达:obj = pickle.loads; obj(x)一等函数值流转不溯源pickle导入本身被 E0066 拦截,危险 callable 无法通过导入获取(redteam_test.go 锁定)。 其他模块上的一等函数流转(如requests包装)仍属 B3 供应链信任。 -
B8 运行时代码注入:
cgitb/importlib/等从外部读取并执行源码的路径。 缓解:only+ 文件系统只读沙箱。
三层防线命名:编译期(escape.go 静态拦截)→ 运行时(_FlySandbox/fly_sb* 动态兜底)→ 进程沙箱(fly-sandboxd seccomp,可选)。两处名单必须逐项同步(redteam_test.go 锁定)。
| # | 逃逸面 | 访问路径 | 编译期 | 运行时 | 处置策略 |
|---|---|---|---|---|---|
| E1 | 属性反射链 | __class__/__bases__/__base__/__mro__/__subclasses__/__globals__/__code__/__closure__/__dict__/__reduce__/__reduce_ex__/__getattribute__/__setattr__/__delattr__/__init_subclass__/__prepare__/__builtins__/__traceback__ |
E0064 反射链扫描(含 f-string 内联二次解析) |
_fly_attr/_fly_get/_fly_set/_fly_getitem/_fly_setitem 黑名单(下标/属性/赋值三通道) |
名单双同步 + 22 组合锁定 |
| E2 | 帧/协程逃逸 |
gi_frame/ag_frame/cr_frame/f_globals/f_locals/__loader__(生成器/协程/帧对象) |
E0064 同名单 | 运行时同名单拦截 | 与 E1 同名单;生成器 send/throw 仅恢复执行、无法注入新名字空间,不构成独立通道(监控名单) |
| E3 | 内建函数面 | eval/exec/compile/open/getattr/globals/locals/vars/input/breakpoint/help/dir/memoryview/__import__ |
E0063 危险内建(任何读取位置) |
_FLY_SB_DANGEROUS 同名拦截 |
双拦截;getattr/globals 属合法高频 API,编译期即拒(见不兼容清单 §6.2) |
| E4 | 模块导入面(黑名单) |
os/sys/subprocess/socket/ctypes/pickle/importlib/marshal/... 80+ 模块 |
E0066 黑名单 |
_FLY_SB_BLOCKED_MODS 同名 |
双名单逐项同步(gen_errorinfo 风格抽查) |
| E5 | 模块导入面(私有实现) |
_json/_ssl/_socket/_ctypes/_thread/... 全部下划线开头模块 |
E0066 规则化:root 以 _ 开头 一律拒 |
_fly_sb_import 同规则 |
规则而非枚举——CPython 私有模块无限增长,枚举不完备 |
| E6 | 模块导入面(白名单外) |
numpy/requests 之外/任意未声明模块(含 site-packages 二进制扩展) |
编译期放行(名单外不拦) |
_fly_sb_import 白名单校验:不在 ALLOWED 一律拒 |
ALLOWED 显式白名单是最终闸门;顶层/函数内 import 均经 gen 注入走 _fly_sb_import(含行列号审计) |
| E7 | sys.modules 缓存读取 |
root in sys.modules 隐式放行(pyexpat 曾经此绕过白名单) |
— |
2026-08 移除,改为 _FLY_SB_ALLOWED_DEP_MODS 显式无害依赖清单(posixpath/ntpath/encodings) |
已修复(escape_binary_mods 反例锁定) |
| E8 | 公开名二进制模块 |
pyexpat/zlib(CPython C 扩展) |
E0066 黑名单新增 |
_FLY_SB_BLOCKED_MODS 新增 |
已修复;公开名 C 扩展走枚举、私有名走 E5 规则 |
| E9 | 模块属性穿透 |
attrgetter/itemgetter(点路径穿透任意对象) |
E0064 编译期拦截(白名单模块对象上) |
_fly_sb_check_modattr 运行时拦截 |
双拦截;绑定名跟踪(modBinds)防别名绕过 |
| E13 | 模块属性面(私有/危险子模块) |
random._os/from random import _os(白名单模块内部绑定的危险模块) |
E0064 _ 前缀模块属性 + E0066 from-import 逐项拦截 |
_fly_sb_check_modattr _ 前缀 + _fly_sb_import fromlist 逐项 |
2026-08 审计新增:from random import _os; _os.system() 可逃逸(编译期/运行时/fromlist 三层);单元素 fromlist/mods 尾逗号元组修复(("a") 字符串 bug) |
| E10 | 内建链重绑定 |
__builtins__ 属性/下标访问 |
E0065 |
_FLY_SB_REFLECT 含 __builtins__
|
双拦截;_FlySandbox/_FlyOnly 均实现 __getitem__(CPython 对非 dict __builtins__ 走下标) |
| E11 | 进程逃逸(OS 面) | 网络/文件/进程/信号 |
cage 编译期注入 |
cage 装饰器 rlimit + fly-sandboxd(可选)clone ns + Landlock + seccomp 白名单 |
双层:rlimit 软限 + 内核级硬限 |
| E12 | 供应链边界 | 白名单模块自身代码(requests/json/...)与 site-packages 编译产物 | 不审查(信任声明) | 模块属性拦截防止白名单模块暴露子模块 | 与 B3 同源;S6 受控包装模式(examples/third_party/) |
| E14 | only 代理 dunder 绕过 | only 块内 __builtins__["__import__"]/__builtins__.__import__ 取原始 builtins.__import__(非 dict __builtins__ 走 __getitem__→__getattr__ dunder 分支放行)→ 绕过 BLOCKED 名单导入 os 并执行 |
E0044:onlyDeny 纳入 __builtins__;onlyDeny ∪ escapeModules 联合拦截(gc/inspect/io 等原缺口) |
_FlyOnly.__getattr__ 特判 __import__ → 返回受限 _fly_sb_import
|
2026-08 审计新增:dunder 分支曾放行原始 builtins.__import__;编译期只拦 onlyDeny 33 项、gc 等 escapeModules 成员可编译通过(运行时仍拦)——现双层补齐 |
审计结论:E1-E14 全部有编译期 + 运行时双覆盖;E7/E8/E13/E14 为 2026-08 审计新增修复。 持续维护要求:CPython 新版本发布时,按"新内建/新模块/新属性"三栏逐项复核本表(见 CI 矩阵)。
Fly 不是"任何合法 Python 都能编译"——合法但违反安全规则的模式会被编译期拦截(编译失败), 按迁移路径分三类:
| 类别 | 拦截模式 | 错误码 | 迁移路径 |
|---|---|---|---|
| 危险 API |
eval/exec/compile/open/getattr/globals/locals/vars/input/dir/memoryview/breakpoint/help/__import__ 的任何读取
|
E0063 | 用安全等价物:getattr→vars()/显式属性(或 only 白名单下受限 hasattr);open→文件读写走 sandbox 约定路径 |
| 反射链 |
obj.__class__.__bases__ 等 22 项属性链、gi_frame 等帧访问 |
E0064 | 数据类/鸭子类型替代反射;框架内省需求走 only 白名单 + 人工评审(B4) |
| 危险模块 | 黑名单 80+ 模块、_ 开头私有模块、白名单外模块(numpy 等) |
E0066 | 白名单内替代(math/json/re/collections...);第三方网络库用 S6 受控包装 |
| 超集例外 | 黑名单模块属性 attrgetter/itemgetter、__builtins__ 访问 |
E0064/E0065 | 显式传参替代 attrgetter 式解包 |
不保证项(照 §6 B1-B8):跨文件污点、通配导入、第三方模块内部、动态反射的字符串拼装形式。 心智模型:现有 Python 代码零改造可编译的占位是"安全子集",超出部分需迁移;8 关键字是超集扩展。 因此对外表述统一为 "Python 3.10+ 的安全受限超集"(README/方案.md 已同步)。
| 维度 | PyFly | Bandit | Semgrep | Ruff (S 规则) |
|---|---|---|---|---|
| 定位 | 语言:安全语义内建,检查是编译的一部分 | 插件:pip 项目扫描器 | 模式匹配引擎 | lint 器安全规则 |
| 检查时机 | 编译期(fly check/fly build/LSP) |
CI 单独步骤 | CI 单独步骤 | 编辑/CI |
| 拦截强度 | 硬失败:编译不过,漏洞版本无法部署 | 软警告:可 # noqa、可跳过、误报多 |
规则可禁用 | 可 ignore |
| 污点跨写法 | 别名/容器/属性/参数传播(§3 R1-R5) | 部分规则(黑名单 API 调用) | 需手写 taint 规则 | 无 taint |
| 误报率取向 | 保守偏安全(R5) | 高(需人工 triage) | 取决于规则质量 | 低但覆盖面窄 |
| 运行时兜底 | 8 关键字注入 + _fly_* 护栏 |
无 | 无 | 无 |
| 形式化模型 | §3 污点规则 R1-R5 + 边界 §6 | 无 | 无 | 无 |
| 与运行时沙箱 | 配套 fly-sandbox(seccomp) | 无 | 无 | 无 |
一句话:Bandit/Semgrep/Ruff 是"扫描提醒",PyFly 是"编译失败"。 前者帮助你发现风险,后者让风险无法进入产物。
PyFly 把"风险代码必须编译失败"作为承诺:通过 fly check 的源码中,
外部输入与危险操作之间必须存在显式清洗;被证明无法静态保证的路径(B1/B4/B7)
如实列出,并用 only 白名单与 fly-sandbox seccomp 做纵深兜底。
2026-08 外部评审(AI 评估)提出的批评中,三条基于对项目实际设计的误读, 在此如实记录并回应;其余真实问题见 §10。
产物恒注入 runtime + sandbox 两节(_FlySandbox 内建代理 + _fly_attr/_fly_get/_fly_set
反射黑名单 + 受限 __import__(BLOCKED 名单)+ 模块属性拦截 + _fly_sb_audit 审计)。
安全保证是双层:编译期拦截(纵深第一层)+ 运行时兜底(第二层)。真正的承诺形态是
"沙箱内 Python 子集"(与 RestrictedPython 同类),而非"纯编译期类型系统"。
澄清后的边界:运行时兜底与产物同体——攻击者改写产物即可绕过运行时层; 因此编译期拦截(safe/mask/lock 零残留)才是不可篡改的保证,运行时层是对 "未改产物但输入恶意"场景的兜底。
checker/escape.go 是对整个 AST 全局的沙箱逃逸扫描(危险内建名出现在任何读取
位置都报 E0063,与代码块内有无关键字无关)。关键字是显式安全策略,escape 是
隐式全局兜底。纯 Python 风格代码同样过全局扫描。
TS 的保证上限由类型擦除决定:类型错误在运行时毫无存在感。Fly 的关键差异是
运行时沙箱是真实强制层(注入的 _FlySandbox 代码不可被业务代码关闭),
保证形态更接近 WASM 的边界模型而非 TS 的编译期检查模型。代价是用户需接受
"我的代码在沙箱里跑"的心智模型,这是文档与教学要持续投入的方向。
| # | 问题 | 状态 | 整改(路线图条目) |
|---|---|---|---|
| G1 | 第三方库内部不透明(B3)+ C 扩展完全不经过管线 | ✅ 已填(试点) | S6 requests 受控包装(examples/third_party/safe_http.fly)+ 信任模型文档(docs/第三方库安全包装.md);C 扩展仍为已知边界 |
| G2 | 8 关键字组合语义无形式化规范(guard+lock 同变量、only 内 seal 等交互) | ✅ 已填 | S4 交互矩阵文档(docs/关键字交互矩阵.md)+ interaction_test.go 22 组合锁定 |
| G3 |
fly update 自更新通道无签名验证——安全语言的自更新是信任链关键 |
✅ 已填 | S2 ed25519 签名:tools/sign + verify.go + release.yml;篡改/缺签拒绝安装 |
| G4 | 零残留关键字(safe/mask/lock/guard)在产物中无痕迹,审计/复盘困难 | ✅ 已填 | S5 --keep-annotations:产物保留 # fly-safe: 等 6 类审计注释 |
| G5 | 无 fuzz / 红队对抗测试——反例测试只覆盖已知模式 | ✅ 已填 | S3 三个 fuzz 目标(lexer/parser/compile,修复 3 个 panic)+ redteam_test.go 19 项对抗;新增属性赋值污点传播 |
| G6 | 错误信息可用性依赖真实用户反馈迭代 | 持续 | 报错清单.md 每码含示例/修复;P6 消息快照 |
| G7 | 类型提示生态割裂(PEP 484 语义冲突的疑问) | ✅ 已澄清 | 文档声明:Fly 不替代类型检查(guard 是值校验);交互矩阵 §组合规则 3 明确 guard 不构成清洗 |
整改顺序:S2(信任链)与 S3(测试盲区)优先于 S4/S5——安全工具自身的 更新通道与对抗测试比功能扩展优先。