Author: RickGray (知道创宇404安全实验室)

近日回顾了 PyCon 2015 上 @Tom Eastman 所讲的关于 Python 序列化格式的安全议题 -《Serialization formats are not toys》。议题主要介绍了 YAML、XML 和 JSON 三种格式用于 Python 序列化数据处理所存在的一些安全问题,其中 XML 部分讲解的是 Python 中的 XXE,而 Python 处理 JSON 数据本身不存在问题,但在前端 JavaScript 对返回 JSON 进行处理时常常直接使用 eval() 来转换类型从而留下安全隐患。

关于 XML 和 JSON 格式相关的安全问题本文就不多提了,本文仅记录下议题中所提到的 Python PyYAML 模块在处理 YAML 格式数据时所存在的问题。

一、Python 中处理 YAML 格式数据

YAML 在数据序列化和配置文件中使用比较广泛,在 Ruby On Rails 中就使用 YAML 作为配置文件。最新的 YAML 标准版本为 1.2,而目前大多数语言对 YAML 解析实现都为 1.1 甚至 1.0 版本,各版本标准可通过官方 yaml.org 进行查阅。一个简单的 YAML 数据为:

--- date: !!str 2016-03-09 weekday: Wednesday weather: sunny plans: &plans 1: daliy resarch 2: daliy meals 3: play games tonight todo: <<: *plans 3: others ...

保存为 sample.yml 然后使用 Python 第三方模块 PyYAML(pip install PyYAML) 来对其进行解析并输出:

import yaml import pprint pprint.pprint(yaml.load(file('sample.yml', 'r')))

运行代码可以得到输出:

(env)? python python test.py {'date': '2016-03-09', 'plans': {1: 'daliy resarch', 2: 'daliy meals', 3: 'play games tonight'}, 'todo': {1: 'daliy resarch', 2: 'daliy meals', 3: 'others'}, 'weather': 'sunny', 'weekday': 'Wednesday'}

PyYAML 在解析数据的时候遇到特定格式的时间数据会将其自动转化为 Python 时间对象,例如 sample.yml 中 date 节点的值使用 !!str 指定其在解析的时候转换为字符串,如果不使用强制类型转换,会自动将 “2016-09-03″ 解析为 Python 中的 datetime.date 对象。如下代码和输出:

import yaml import pprint content = '''--- date: 2016-03-09 ...''' pprint.pprint(yaml.load(content)) # (env)? python python test1.py # {'date': datetime.date(2016, 3, 9)}

(本文重点不在 YAML 格式上,详情可参考官方文档和 wiki)

二、PyYAML 特有类型解析 Python 对象

除开 YAML 格式中常规的列表、字典和字符串整形等类型转化外,各个语言的 YAML 解析器或多或少都会针对其语言实现一套特殊的对象转化规则。例如 Ruby 中可以将类对象 dump 为 YAML 格式的文本数据(文件 person.rb):

require 'yaml' class Person attr_accessor :name, :age, :children def initialize(name, age, children=nil) @name = name @age = age @children = children end end children = [Person.new('John Smith', 12), Person.new('Jan Smith', 11)] tom = Person.new('Tom Smith', 23, children) File.open('sample2.yml', 'w') do |os| YAML::dump(tom, os) end

运行脚本得到输出为(为了突入结构将其格式化,默认情况缩紧不严谨):

!ruby/object:Person name: Tom Smith age: 23 children: - !ruby/object:Person name: John Smith age: 12 children: - !ruby/object:Person name: Jan Smith age: 11 children:

其中 !ruby/object:Person 指代的是 person.rb 中的 Person 类,是 Ruby 里 yaml 模块针对 Ruby 语言的特有实现,如果使用其他语言的 YAML 解析器来加载这段 YAML 文本必定会报错。不同语言针对 YAML 基本都有一套其对语言对象的解析扩展,这也是 YAML 在各语言之间兼容性差的原因之一。

而在 Python 中,一个对象序列化为 YAML 数据是这个样子的:

import yaml class Person(object): def __init__(self, name, age, sponse=None, children=None): self.name = name self.age = age self.sponse = sponse self.children = children jane = Person('Jane Smith', 25) children = [Person('Jimmy Smith', 15), Person('Jenny Smith', 12)] john = Person('John Smith', 37, jane, children) print yaml.dump(john) print yaml.dump(open('sample.yml', 'r'))

运行脚本输出结果为:

(env)? python python person.py !!python/object:__main__.Person age: 37 children: - !!python/object:__main__.Person {age: 15, children: null, name: Jimmy Smith, sponse: null} - !!python/object:__main__.Person {age: 12, children: null, name: Jenny Smith, sponse: null} name: John Smith sponse: !!python/object:__main__.Person {age: 25, children: null, name: Jane Smith, sponse: null} !!python/object:__builtin__.file {}

可以看到 !!python/object:main.Person 为 PyYAML 中对 Python 对象的类型转化标签,在解析时会将后面的值作为 Person 类的实例化参数进行对象还原。在上面的测试代码中特地 dump 了一下文件对象 open(‘sample.yml’, ‘r’) ,在 YAML 中对应的数据为 !!python/object:builtin.file {} ,这里参数为空,其实通过 PyYAML load() 还原回去会发现是一个为初始化参数并已经处于关闭状态的畸形 file 实例对象。

然而看到 builtin 这个关键字就应该敏感起来,通过查看 PyYAML 源码可以得到其针对 Python 语言特有的标签解析的处理函数对应列表($PYTHON_HOME/lib/site-packages/yaml/constructor.py 612 – 674 行):

!!python/none => Constructor.construct_yaml_nul !!python/bool => Constructor.construct_yaml_boo !!python/str => Constructor.construct_python_str !!python/unicode => Constructor.construct_python_unicode !!python/int => Constructor.construct_yaml_int !!python/long => Constructor.construct_python_long !!python/float => Constructor.construct_yaml_float !!python/complex => Constructor.construct_python_complex !!python/list => Constructor.construct_yaml_seq !!python/tuple => Constructor.construct_python_tuple !!python/dict => Constructor.construct_yaml_map !!python/name: => Constructor.construct_python_name !!python/module: => Constructor.construct_python_module !!python/object: => Constructor.construct_python_object !!python/object/apply: => Constructor.construct_python_object_apply !!python/object/new: => Constructor.construct_python_object_new