datamodel-code-generator:一行 weak import,为什么能写到目录之外
原创
云梦DC
云梦DC
云梦安全
2026年10月29日 11:21
河南
去阅读
2026 年 10 月披露的 CVE-2026-107377(GHSA-77xj-x4rm-935c,CVSS 7.5)出在 datamodel-code-generator 的 Protobuf 输入处理。这个工具常被放进代码生成流水线,把 JSON Schema、OpenAPI、XSD 或 .proto 转成 Python 模型。输入经常来自上游仓库、URL 或第三方交付物,未必都可信。
本地用 0.80.0 跑一份带恶意 import weak 的 schema,protoc 最后报错退出了,但目录外的文件已经写出来,另一个已有文件也被覆盖。换到 0.81.0,生成器在写入前抛出 Invalid Protocol Buffers weak import path,磁盘没有变化。
漏洞速览
| 项目 | 内容 |
|---|---|
| 漏洞编号 | CVE-2026-107377 / GHSA-77xj-x4rm-935c |
| 影响组件 | datamodel-code-generator 0.59.0–0.80.0 |
| 修复版本 | 0.81.0 |
| 漏洞类型 | 路径穿越导致任意文件写入(CWE-22) |
| 危害等级 | CVSS 7.5 High |
| 触发前提 | 对不可信 .proto 执行 protobuf 代码生成 |
weak import 的补文件逻辑
Protobuf 的 import weak 表示依赖可以缺失。为了让 protoc 继续编译,生成器会在临时目录里为每个 weak import 补一个空壳文件,里面只有一行 syntax 声明,再把这个目录加入 include path。
问题在文件名直接取自 schema:
WEAK_IMPORT_PATTERN = re.compile(r'^\\s*import\\s+weak\\s+”([^”]+)”\\s*;', re.MULTILINE) stub = self.weak_import_dir / import_pathstub.parent.mkdir(parents=True, exist_ok=True)stub.write_text(syntax, encoding=self.parser.encoding)
正则的捕获组没有限制字符,import\_path 可以包含 /、..,也可以是绝对路径。pathlib 的 / 运算符在这里有两个关键行为:右操作数是绝对路径时,左边整个被丢掉;.. 不会被自动拦下或规范化。mkdir(parents=True) 还会把中间目录一起建出来。
于是写入位置可以落到临时目录之外。比如:
import weak ”/home/user/.config/x”;import weak ”../../victim.txt”;
第一条拼出绝对路径,第二条从临时目录往上爬两级。
检查和写入用的不是同一个位置
写文件之前,代码确实做了一次存在性检查:
if any((include_path / import_path).exists() for include_path in include_paths): continue
但 include\_paths 是“输入文件所在目录 + 临时目录”等候选路径,真正写入用的是 weak\_import\_dir / import\_path。检查是按其中某个目录算的,写入是按临时目录算的,两边并不一定指向同一个文件。
绝对路径的表现比较特殊。include\_path / \"/abs/x\" 无论左边是什么,结果都是 /abs/x,所以目标已存在时会被跳过。它更适合新建文件,不容易覆盖现有文件。
相对路径 ../../victim.txt 就不一样了。检查时从输入目录往上爬,写入时从临时目录往上爬,两个路径可以指向不同文件。攻击者只要让检查目标不存在、写入目标存在,就能绕过检查完成覆盖。
这里的根因是路径基准不一致,不涉及并发时序。校验和真正落地如果不用同一个已经解析、规范化的路径,检查通过并不能说明写入安全。
0.80.0 和 0.81.0 的磁盘结果
实验准备了一份 evil.proto,同时放绝对路径和 ../../victim.txt 两条 weak import。目标目录里先放 victim.txt,内容为 ORIGINAL BUILD CONFIG。
0.80.0 执行后,protoc 报了三条错:文件不存在、虚拟路径不允许 ..、import 无法解析,进程以非零码退出。但在这之前,abs\_pwn.txt 已经被创建,内容是 syntax = \"proto3\";;victim.txt 也被同一行内容覆盖。
这一点在排查时容易漏掉。构建失败只说明后续步骤没走通,前面的文件系统副作用已经发生了。看 CI 日志时,不能因为最后一条是 protoc 报错就认定没有写入。
0.81.0 在检查阶段抛 Invalid Protocol Buffers weak import path。abs\_pwn.txt 不存在,victim.txt 还是原来的内容。
影响边界要说清楚
写入内容不可控,只有一行 syntax = \"proto2\"; 或 syntax = \"proto3\";,公告也没有演示出直接代码执行。把它直接说成“任意文件写入导致 RCE”不准确。
但完整性影响仍然存在。构建关键文件可能被覆盖,比如 init.py、锁文件、入口脚本、CI 配置;目录树可以被任意创建,文件也能落到被监听或自动加载的位置。生成器进程如果有制品仓库凭据、部署密钥或云平台令牌,攻击者还可以把一次构建破坏拆成后续几步。
触发条件也不算苛刻。流水线只要处理一个第三方 .proto,或者通过 --url 拉取远程 schema,就会走到这段逻辑。代码生成又常跑在构建节点上,权限通常比普通业务容器高。
0.81.0 的修复顺序
0.81.0 会把临时目录和候选 stub 路径都做 resolve(),再判断候选路径是否仍位于临时目录内。不在范围内就抛 SchemaParseError。这个判断放在创建父目录、写文件之前,输入准备失败时临时目录也会清理。
路径校验的顺序很关键。先解析、规范化,再判断包含关系,最后才创建和写入。反过来先建目录、先打开文件,再补检查,副作用已经留下了。
需要覆盖的输入形态至少有三类:绝对路径、带 .. 的相对路径,以及符号链接。前两类是这次公告的重点;符号链接要在 resolve() 之后判断目标位置,否则链接指向的仍可能是目录外。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论