ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CPython 3.6.2rc2 安全修复解析:subprocess 环境变量注入、expat 漏洞升级与 urllib.parse 主机解析加固

CPython 3.6.2rc2 安全修复解析:subprocess 环境变量注入、expat 漏洞升级与 urllib.parse 主机解析加固 CPython 3.6.2rc2 安全修复解析subprocess 环境变量注入、expat 漏洞升级与 urllib.parse 主机解析加固【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython导读CPython 3.6.2rc2 于 2017-07-07 发布这是一次以“安全修复”为核心的候选版本Release Candidate。本文基于 Misc/NEWS.d/3.6.2rc2.rst 中的三条 Security 级变更记录逐一拆解其技术背景、漏洞原理与修复手段包括 Windows 下subprocess环境变量注入防护、内嵌 expat 库从 2.2.0 升级至 2.2.1 所涵盖的多个 CVE 修复以及urllib.parse.splithost()对 URL fragment 解析的纠错。读完本文你将理解这三类安全问题的成因、CPython 官方修复策略以及如何在当前仓库源码与测试用例中验证这些修复。一、修复总览一次候选版本中的三项 Security 级变更3.6.2rc2.rst是 CPython 采用 blurb 工具管理的 NEWS 片段文件Misc/NEWS.d目录下每个.rst文件对应一条新闻条目。该文件记录了 3.6.2rc2 中三条 Security 级别的变更对应三个 bpo issuebpo 编号影响模块核心内容分类bpo-30730subprocessWindows阻止环境变量注入防止父进程环境变量与命令行参数被意外透传Securitybpo-30694Modules/expat内嵌 expat 从 2.2.0 升级到 2.2.1修复多个安全漏洞Securitybpo-30500urllib.parse修复splithost()对 fragment#与loginhost形式的解析错误Security三条变更全部标注为.. section: Security说明它们面向的是可被攻击者利用的输入处理缺陷而非普通的功能性 bug。下面分别展开。二、bpo-30730阻止 Windows 下 subprocess 的环境变量注入2.1 漏洞背景NEWS 片段原文Prevent environment variables injection in subprocess on Windows. Prevent passing other environment variables and command arguments.该问题源于 Windows 进程创建机制的特殊性。在 Windows 上subprocess通过CreateProcess系列 API 启动子进程环境块environment block和命令行参数以单块内存的形式传递当调用方传入自定义env字典时若实现不当可能出现本应被隔离的环境变量被“注入”到子进程环境或额外的命令行参数被附带传入从而造成权限提升或任意参数执行等安全后果。这与 POSIX 平台上 forkexec 语义下环境与参数天然隔离的情况不同。2.2 源码侧的防护实现从当前仓库的 Lib/subprocess.py 可以看到Windows 分支的_execute_child()即_execute_child(self, args, executable, preexec_fn, close_fds, pass_fds, cwd, env, startupinfo, creationflags, shell, ...)位于第 1617 行起承担了核心的进程启动逻辑第 1629 行断言pass_fds在 Windows 上不被支持assert not pass_fds第 1631–1643 行将args按 Windows 规则转换为命令行字符串使用list2cmdline()处理列表形式的参数避免参数拼接错误导致意外参数注入第 1649–1654 行对STARTUPINFO做拷贝bpo-34044 之后的行为确保多次复用startupinfo时不会因就地修改而泄漏句柄或标志第 1656–1661 行仅在需要时设置STARTF_USESTDHANDLES精确控制哪些句柄被继承。与此同时环境变量的合法性校验在 POSIX 与 Windows 通用路径中都有体现当env非空时第 2068–2076 行会对每个键值对做os.fsencode()编码并拒绝名称中包含的非法环境变量第 2072–2073 行raise ValueError(illegal environment variable name)。这类校验可以防止通过畸形键名向子进程环境块注入额外条目。2.3 测试用例验证Lib/test/test_subprocess.py 中保留了大量与本次修复相关的回归测试test_one_environment_variable()第 854 行仅传入{fruit: orange}一个环境变量验证子进程中读取到的正是该变量Windows 下通过CMD /c SET fruit验证确保不会混入调用方其他环境变量test_invalid_env()第 879 行分别构造环境变量名含\0、值含\0、名称含三种畸形env字典断言subprocess.Popen抛出ValueError从测试层面锁定了“环境变量不能携带额外分隔符或参数”的约束test_empty_env()第 827 行及相关注释第 818 行起说明 Windows 下空环境可能导致系统无法启动子进程需要SYSTEMROOT并在 CI 环境AddressSanitizer下对继承行为做了额外处理这些注释直接体现了该修复针对 Windows 环境语义所做的权衡。运行方式# 在已构建的 CPython 源码树中执行相关回归测试 ./python -m test test_subprocess -m test_invalid_env ./python -m test test_subprocess -m test_one_environment_variable三、bpo-30694内嵌 expat 由 2.2.0 升级至 2.2.13.1 涉及的安全漏洞清单NEWS 片段原文列出了本次升级所修复的多个 CVECVE漏洞类型说明CVE-2017-9233外部实体无限循环 DoS通过外部实体External entity构造无限循环造成拒绝服务CVE-2016-9063整数溢出再次修复2.2.0 修复后的回归问题需二次修复CVE-2016-07182.2.0 修复的回归 bug由对 CVE-2016-0718 的修复引入的回归在 2.2.1 中一并修复CVE-2012-0876使用 SipHash 应对计数器哈希洪水通过可预测哈希导致哈希洪水攻击CVE-2016-5300使用getrandom等 OS 熵源对 Python 无影响理由见下3.2 为什么 CVE-2016-5300 不影响 Python这是本条目中最值得注意的细节。NEWS 原文特别说明Note: the CVE-2016-5300 (Use os-specific entropy sources like getrandom) doesnt impact Python, since Python already gets entropy from the OS to set the expat secret usingXML_SetHashSalt().即expat 的哈希盐hash salt原本应使用操作系统熵源生成但 Python 在调用 expat 时已经通过XML_SetHashSalt()使用操作系统提供的熵设置了该盐值因此即使底层 expat 自身未升级熵源实现Python 侧的哈希随机化防线也已经到位。这解释了为何该 CVE 列入清单却不构成 Python 的暴露面。3.3 当前仓库中的 expat 版本印证仓库将 expat 以内部副本形式内嵌于 Modules/expat 目录。在 Modules/expat/expat.h 中可以看到当前内嵌版本的版本号宏#define XML_MAJOR_VERSION 2 #define XML_MINOR_VERSION 8 #define XML_MICRO_VERSION 4即当前仓库携带的 expat 已远高于 3.6.2rc2 时代的 2.2.1验证了 CPython 长期坚持“内嵌 expat 并持续同步上游安全修复”的策略——每次 expat 上游发布安全版本CPython 都会同步升级以修复 XML 解析相关的 CVE。Python 标准库侧对 expat 的封装位于 Modules/pyexpat.cpyexpat/xml.parsers.expat的 C 实现哈希盐的设置与XML_SetHashSalt()的调用也在此模块与 expat 的交互路径中完成。若需快速验证当前构建所链接的 expat 版本可执行./python -c import pyexpat; print(pyexpat.EXPAT_VERSION)四、bpo-30500修复 urllib.parse.splithost() 的 fragment 解析4.1 漏洞描述与修复语义NEWS 片段原文Fix urllib.parse.splithost() to correctly parse fragments. For example,splithost(//127.0.0.1#evil.com/)now correctly returns the127.0.0.1host, instead of treatingevil.comas the host in an authentication (loginhost).修复前splithost(//127.0.0.1#evil.com/)会把127.0.0.1#evil.com整体当作“登录信息 主机”从而错误地认为主机是evil.com而实际上#之后的内容是 URL fragment片段不应参与主机解析。修复后#被识别为 fragment 起始符主机正确解析为127.0.0.1。这一缺陷之所以被归类为 Security在 URL 白名单校验、SSRF 防护等场景中如果主机解析被 fragment 中的伪主机名误导可能绕过校验把请求发往非预期地址。4.2 源码实现从 splithost 到 _splithost在 Lib/urllib/parse.py 中splithost的实现如下第 1257–1261 行def splithost(url): warnings.warn(urllib.parse.splithost() is deprecated as of 3.8, use urllib.parse.urlsplit() instead, DeprecationWarning, stacklevel2) return _splithost(url)底层函数_splithost第 1264–1277 行使用正则切分主机与路径_hostprog None def _splithost(url): splithost(//host[:port]/path) -- host[:port], /path. global _hostprog if _hostprog is None: _hostprog re.compile(//([^/#?]*)(.*), re.DOTALL) match _hostprog.match(url) if match: host_port, path match.groups() if path and path[0] ! /: path / path return host_port, path return None, url关键点在于字符类[^/#?]*主机部分在遇到/、#、?三个字符时即停止匹配。这意味着#之后的 fragment 不再被纳入主机名从而修复了将#evil.com误判为loginhost形式的问题。正则中的#、?与/并列也保证了 fragment、query 与路径都能正确地从主机字段中剥离。注意自 Python 3.8 起公开的splithost()已发出DeprecationWarning官方建议改用urllib.parse.urlsplit()修复后的解析逻辑保留在内部函数_splithost中测试也直接针对_splithost编写。4.3 回归测试fragment 与主机解析Lib/test/test_urlparse.py 的test_splithost()第 1777 行起完整记录了本次修复的预期行为def test_splithost(self): splithost urllib.parse._splithost self.assertEqual(splithost(//www.example.org:80/foo/bar/baz.html), (www.example.org:80, /foo/bar/baz.html)) self.assertEqual(splithost(//www.example.org:80), (www.example.org:80, )) self.assertEqual(splithost(/foo/bar/baz.html), (None, /foo/bar/baz.html)) # bpo-30500: # starts a fragment. self.assertEqual(splithost(//127.0.0.1#host.com), (127.0.0.1, /#host.com)) self.assertEqual(splithost(//127.0.0.1#host.com:80), (127.0.0.1, /#host.com:80)) self.assertEqual(splithost(//127.0.0.1:80#host.com), (127.0.0.1:80, /#host.com))代码注释# bpo-30500: # starts a fragment.直接标注了与本次修复对应的断言覆盖了「带端口 fragment」「纯 fragment 含端口号」等多种组合。此外test_splituser()第 1806 行验证了loginhost形式的用户信息解析仍然正确说明修复 fragment 解析并未破坏认证信息的既有语义test_splithost_deprecation()第 1926 行验证了公开 API 的弃用警告行为。运行验证./python -m test test_urlparse -m test_splithost五、从三次修复看 CPython 的安全工程实践将三条 NEWS 记录放在一起可以提炼出 CPython 在安全维护上的几个可复用的工程模式安全修复与版本节奏解耦Security 级别的修复可以不等待下一个完整功能版本而是在 rc 版本中提前合入并随候选版发布缩短漏洞暴露窗口。3.6.2rc2 正体现了“候选版本同样承担安全修复”的发布策略。外部依赖内嵌并持续同步expat 以内部副本形式存放在 Modules/expat每次上游发布安全版本即整体同步。此类内嵌库的安全状态属于“跟随上游 Python 侧补强”如用XML_SetHashSalt()提前补上熵源缺口的双层防线。解析类漏洞以回归测试固化无论是 Windows 环境块注入test_invalid_env还是 URL 主机解析test_splithost中带# bpo-30500注释的断言都以明确的单元测试锁定预期行为防止后续重构导致漏洞复发。历史 API 的安全语义以内部函数延续splithost的修复落实到内部函数_splithost公开 API 进入弃用流程并引导用户迁移到urlsplit()既修复了安全缺陷又完成了 API 演进。六、如何在本仓库中复现与验证本仓库是 CPython 源码树可先按 README.rst 完成构建Unix 类平台通常为./configure make然后按上文各节给出的命令运行对应测试# 验证 subprocess 环境变量防护 ./python -m test test_subprocess -m test_invalid_env # 验证 splithost fragment 解析 ./python -m test test_urlparse -m test_splithost # 查看当前内嵌 expat 版本 ./python -c import pyexpat; print(pyexpat.EXPAT_VERSION)相关的关键文件路径汇总变更记录Misc/NEWS.d/3.6.2rc2.rstsubprocess 实现与测试Lib/subprocess.py、Lib/test/test_subprocess.pyurllib.parse 实现与测试Lib/urllib/parse.py、Lib/test/test_urlparse.py内嵌 expat 源码Modules/expat版本宏见 Modules/expat/expat.h、封装模块 Modules/pyexpat.c结语CPython 3.6.2rc2 的三项 Security 修复分别覆盖了进程环境隔离subprocess、第三方 XML 解析器漏洞面expat与 URL 解析语义urllib.parse恰好对应了标准库中三类最容易遭受攻击者构造输入的攻击面。理解这些修复的成因与落地方式不仅能帮助你评估旧版本 Python 的安全风险也能为你在自研代码中处理环境变量、URL 解析和依赖同步时提供直接可借鉴的防御思路。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进