CVE-2019-9740 Python urllib CRLF injection vulnerability 浅析
字数 1189 2025-08-29 08:32:24

Python urllib CRLF 注入漏洞分析 (CVE-2019-9740)

漏洞概述

CVE-2019-9740 是 Python urllib 库中的一个 CRLF 注入漏洞,允许攻击者通过精心构造的 URL 在 HTTP 请求头中注入恶意内容。该漏洞是 2016 年 CVE-2016-5699 漏洞的变种,尽管之前已经修复了部分问题,但仍存在可利用的注入点。

CRLF 注入原理

CRLF 是"回车+换行"(\r\n)的简称,十六进制码为 0x0d0x0a。在 HTTP 协议中:

  • HTTP 头和 HTTP 体使用两个 \r\n 分隔
  • 浏览器根据这些控制字符来解析和显示 HTTP 内容
  • 攻击者若能控制 HTTP 消息头中的字符,就可以注入恶意换行来操纵会话 Cookie 或 HTML 体

漏洞演示

正常请求示例

import urllib.request

url = "http://127.0.0.1"
response = urllib.request.urlopen(url)

正常请求头:

GET / HTTP/1.1
Host: 127.0.0.1
User-Agent: Python-urllib/3.7
...

恶意请求示例

import urllib.request

host = "127.0.0.1%0d%0a%0d%0aheaders:test"
url = "http://" + host
response = urllib.request.urlopen(url)

注入后的请求头:

GET / HTTP/1.1
Host: 127.0.0.1

headers:test
User-Agent: Python-urllib/3.7
...

漏洞分析

漏洞代码路径

  1. urllib.request.urlopen() 是入口函数
  2. 调用 build_opener() 创建 opener
  3. 使用 HTTPHandler 处理 HTTP 请求
  4. 最终通过 http.client.HTTPConnection 发送请求

关键问题点

http/client.py 中的 putheader 方法:

def putheader(self, header, *values):
    values = list(values)
    for i, one_value in enumerate(values):
        if hasattr(one_value, 'encode'):
            values[i] = one_value.encode('latin-1')
        elif isinstance(one_value, int):
            values[i] = str(one_value).encode('ascii')
    value = b'\r\n\t'.join(values)
    header = header + b': ' + value
    self._output(header)

问题在于:

  1. 只检查了响应头中的 CRLF,没有检查发送的 URL
  2. 允许在 URL 中嵌入 CRLF 控制字符

修复后的代码

修复后的 putheader 方法增加了严格的头部验证:

def putheader(self, header, *values):
    if self.__state != _CS_REQ_STARTED:
        raise CannotSendHeader()

    if hasattr(header, 'encode'):
        header = header.encode('ascii')

    if not _is_legal_header_name(header):
        raise ValueError('Invalid header name %r' % (header,))

    values = list(values)
    for i, one_value in enumerate(values):
        if hasattr(one_value, 'encode'):
            values[i] = one_value.encode('latin-1')
        elif isinstance(one_value, int):
            values[i] = str(one_value).encode('ascii')

        if _is_illegal_header_value(values[i]):
            raise ValueError('Invalid header value %r' % (values[i],))

    value = b'\r\n\t'.join(values)
    header = header + b': ' + value
    self._output(header)

新增的验证函数:

_is_legal_header_name = re.compile(rb'[^:\s][^:\r\n]*').fullmatch

官方修复方案

官方在 putrequest 方法中增加了对 URL 的严格检查:

  • 匹配所有 ASCII 码在 00 到 32 的控制字符
  • 同时匹配 \x7f 字符

修复提交:
https://github.githistory.xyz/python/cpython/blob/96aeaec64738b730c719562125070a52ed570210/Lib/http/client.py

漏洞影响

  • 允许攻击者操纵 HTTP 请求头
  • 可能导致 HTTP 请求走私、缓存投毒、跨站脚本等攻击
  • 影响所有使用 Python urllib 库的应用程序

防护建议

  1. 升级到修复后的 Python 版本
  2. 对用户提供的 URL 进行严格验证
  3. 避免直接使用用户输入构造 URL
  4. 使用更现代的请求库如 requests 替代 urllib

参考链接

  1. https://bugs.python.org/issue36276
  2. https://hg.python.org/cpython/rev/bf3e1c9b80e9
  3. https://bugs.python.org/issue30458#msg295067
Python urllib CRLF 注入漏洞分析 (CVE-2019-9740) 漏洞概述 CVE-2019-9740 是 Python urllib 库中的一个 CRLF 注入漏洞,允许攻击者通过精心构造的 URL 在 HTTP 请求头中注入恶意内容。该漏洞是 2016 年 CVE-2016-5699 漏洞的变种,尽管之前已经修复了部分问题,但仍存在可利用的注入点。 CRLF 注入原理 CRLF 是"回车+换行"( \r\n )的简称,十六进制码为 0x0d 和 0x0a 。在 HTTP 协议中: HTTP 头和 HTTP 体使用两个 \r\n 分隔 浏览器根据这些控制字符来解析和显示 HTTP 内容 攻击者若能控制 HTTP 消息头中的字符,就可以注入恶意换行来操纵会话 Cookie 或 HTML 体 漏洞演示 正常请求示例 正常请求头: 恶意请求示例 注入后的请求头: 漏洞分析 漏洞代码路径 urllib.request.urlopen() 是入口函数 调用 build_opener() 创建 opener 使用 HTTPHandler 处理 HTTP 请求 最终通过 http.client.HTTPConnection 发送请求 关键问题点 在 http/client.py 中的 putheader 方法: 问题在于: 只检查了响应头中的 CRLF,没有检查发送的 URL 允许在 URL 中嵌入 CRLF 控制字符 修复后的代码 修复后的 putheader 方法增加了严格的头部验证: 新增的验证函数: 官方修复方案 官方在 putrequest 方法中增加了对 URL 的严格检查: 匹配所有 ASCII 码在 00 到 32 的控制字符 同时匹配 \x7f 字符 修复提交: https://github.githistory.xyz/python/cpython/blob/96aeaec64738b730c719562125070a52ed570210/Lib/http/client.py 漏洞影响 允许攻击者操纵 HTTP 请求头 可能导致 HTTP 请求走私、缓存投毒、跨站脚本等攻击 影响所有使用 Python urllib 库的应用程序 防护建议 升级到修复后的 Python 版本 对用户提供的 URL 进行严格验证 避免直接使用用户输入构造 URL 使用更现代的请求库如 requests 替代 urllib 参考链接 https://bugs.python.org/issue36276 https://hg.python.org/cpython/rev/bf3e1c9b80e9 https://bugs.python.org/issue30458#msg295067