Tornado 6.3.2 安全修复解析:StaticFileHandler 开放重定向漏洞的成因、修复与验证
【免费下载链接】tornadoTornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed.项目地址: https://gitcode.com/gh_mirrors/to/tornado
导读
本文围绕 Tornado 6.3.2(发布于 2023 年 5 月 13 日)的唯一一项变更展开:修复StaticFileHandler在特定配置下存在的开放重定向(open redirect)漏洞。我们将从漏洞的协议原理入手,结合 tornado/web.py 中的修复实现与 tornado/test/web_test.py 中的回归测试,还原漏洞触发条件、修复策略与验证方法,帮助读者理解静态文件服务场景下的 URL 安全边界,并给出可落地的排查与升级建议。
Tornado 6.3.2:一次聚焦安全的补丁版本
Tornado 6.3.2 是 6.3 系列的一个维护版本,发布说明极其精炼,核心内容仅一条:
Fixed an open redirect vulnerability in StaticFileHandler under certain configurations.(修复了 StaticFileHandler 在特定配置下的开放重定向漏洞)
对比相邻版本(6.3.1 修复的是RequestHandler.set_cookie对大写关键字参数的兼容性问题),6.3.2 的变更范围更集中——它完全围绕静态文件服务的 URL 规范化逻辑展开。该漏洞不是代码注入或路径穿越,而是一个HTTP 重定向语义问题:在满足特定配置组合时,攻击者可以诱导服务器发出指向任意外部主机的 302/301 重定向,进而用于钓鱼、凭据窃取等攻击场景。
什么是开放重定向(Open Redirect)
开放重定向指服务器在收到请求后,将用户重定向到攻击者可控的外部 URL。其危害在于:浏览器地址栏中的域名仍是受害站点,用户会误以为链接可信,从而在钓鱼页面上泄露敏感信息。Tornado 的RequestHandler.redirect()(见 tornado/web.py)本身允许相对或绝对 URL,因此框架层面对重定向目标并无白名单约束——安全责任落在生成重定向目标的调用方身上。
在 6.3.2 之前的StaticFileHandler中,当请求一个不带尾斜杠的目录路径且配置了默认首页文件时,处理器会执行:
self.redirect(self.request.path + "/", permanent=True)即把/static/dir301 重定向到/static/dir/。该重定向目标是请求路径原样拼接斜杠,于是问题转化为:请求路径能否被构造得形如//evil.com/...?这正是本漏洞的关键——//开头的路径在浏览器中会被解析为协议相对 URL(protocol-relative URL),例如Location: //evil.com/static/dir/会被浏览器当作https://evil.com/static/dir/处理,从而将用户引导至攻击者控制的站点。
漏洞触发条件:三项配置缺一不可
根据 tornado/test/web_test.py 中回归测试的注释,漏洞只在满足以下全部条件的特定配置下可被利用:
| 条件 | 说明 | 对应配置 |
|---|---|---|
| 1 | 静态 URL 前缀为/(根路径) | static_url_prefix="/" |
| 2 | 设置了目录默认文件 | static_handler_args=dict(default_filename="index.html")(任意值均可) |
| 3 | 攻击者已知静态目录在服务器上的(近)绝对路径 | 例如测试中的os.path.abspath("static") |
前两项是 Tornado 应用侧的配置组合:当static_url_prefix="/"时,路由不再为静态路径保留独立前缀,请求路径会原样进入StaticFileHandler的路径校验逻辑;default_filename则让"目录 + 无尾斜杠"的请求进入重定向分支(见 tornado/web.py)。第三项是攻击成本——攻击者需要知道服务端静态目录的绝对路径,才能构造出既能通过路径包含校验、又能在规范化后变成//host开头的畸形路径,例如测试中的//evil.com/../{absolute_static_dir}/static/dir:
- 服务器端
os.path对路径做规范化和拼接后,//evil.com/../...中//evil.com被当作一个普通路径段,..回退后落在静态目录内,通过了validate_absolute_path的越界检查(tornado/web.py); - 但该路径不以尾斜杠结尾且对应一个目录,于是进入重定向分支:
self.request.path + "/"得到//evil.com/../.../dir/,写入Location响应头; - 浏览器将
//evil.com识别为协议相对 URL 的主机部分,把用户重定向到evil.com。
值得注意的是,此路径并不需要真实存在于磁盘,它只需满足"规范化后对应一个目录"的判定即可,因此攻击面相对容易扩大。
修复实现:在重定向分支前置 403 拦截
修复落在 tornado/web.py 的validate_absolute_path方法中,紧贴重定向代码之前:
if not self.request.path.endswith("/"): if self.request.path.startswith("//"): # A redirect with two initial slashes is a "protocol-relative" URL. # This means the next path segment is treated as a hostname instead # of a part of the path, making this effectively an open redirect. # Reject paths starting with two slashes to prevent this. # This is only reachable under certain configurations. raise HTTPError( 403, "cannot redirect path with two initial slashes" ) self.redirect(self.request.path + "/", permanent=True) return None修复策略是在触发重定向前显式拒绝:任何以//开头的请求路径,直接返回 HTTP 403 Forbidden,而非发起重定向。从源码注释可以读出两层设计意图:
- 语义清晰:
//开头的路径一旦进入重定向,就会变成协议相对 URL,属于不可接受的重定向目标,因此在源头掐断; - 注释诚实标注适用边界:代码注释明确指出 "This is only reachable under certain configurations",说明该分支与
static_url_prefix="/"等配置强相关,并非所有部署都会走到。
此外,403 响应本身也避免了信息泄露:它发生在重定向前,攻击者无法再通过"重定向 vs 403"的行为差异探测静态目录是否存在——这与该方法中_resolve_symlink_target越界时"故意返回与路径检查相同错误"的设计(tornado/web.py)一脉相承,体现了 Tornado 在路径校验上"错误信息不区分内外"的安全一致性。
回归测试:锁定漏洞场景的精确验证
static_foo.txt 所在测试目录配合StaticDefaultFilenameRootTest测试类(tornado/test/web_test.py)完成了对修复的验证:
def test_no_open_redirect(self): # 6.3.2 之前影响某些配置的开放重定向已不可能再发生 # 漏洞需要 static_url_prefix="/" 与 default_filename(任意值)同时设置, # 并且需要知道静态目录的服务器端绝对路径。 test_dir = os.path.dirname(__file__) drive, tail = os.path.splitdrive(test_dir) if os.name == "posix": self.assertEqual(tail, test_dir) else: test_dir = tail with ExpectLog(gen_log, ".*cannot redirect path with two initial slashes"): response = self.fetch( f"//evil.com/../{test_dir}/static/dir", follow_redirects=False, ) self.assertEqual(response.code, 403)测试要点有三处,均值得在生产排查时参考:
- 测试夹具完全复刻漏洞配置:
static_path=os.path.abspath(...)配合static_url_prefix="/"与default_filename="index.html",确保测试覆盖的是真正受影响的那类部署; - HTTP 客户端特意选用
SimpleAsyncHTTPClient:测试注释解释了原因——curl 客户端根本不允许发送以两个斜杠开头的请求,而simple_httpclient可以,这保证畸形请求能真实到达服务器端处理链(tornado/test/web_test.py); - 断言 403 且关闭重定向跟随:
follow_redirects=False确保验证的是服务器原始响应;ExpectLog同时确认了修复分支中"cannot redirect path with two initial slashes"的日志路径被实际执行,即修复代码真正生效。
防御纵深与升级建议
针对使用 Tornado 静态文件服务的项目,结合本次修复可以沉淀以下实践:
- 升级到 6.3.2 及以上版本:该修复已进入 6.3.2 及其后的 6.4.x、6.5.x 系列,升级是消除漏洞最直接的途径。仓库根目录的 README.rst 与 pyproject.toml 提供了项目安装与版本管理入口。
- 审视自身配置组合:若应用同时使用了
static_url_prefix="/"与default_filename,应重点核查是否暴露在公开网络;测试目录tornado/test/static/dir/index.html与 tornado/test/static/robots.txt 展示了默认静态目录的典型布局,便于对照。 - 理解重定向前置检查的必要性:
RequestHandler.redirect对目标 URL 不做协议白名单,任何基于"用户可控路径 + 字符串拼接"生成Location的代码都应当自行校验目标(拒绝//开头、拒绝javascript:等协议),本次修复正是这一原则在框架内部的落实。 - 保持"错误行为不泄露内部信息"的原则:Tornado 在本次修复及 symlink 校验中均刻意让 403 行为一致,避免攻击者通过响应差异探测文件系统结构,这也是安全加固中值得借鉴的通用做法。
小结
Tornado 6.3.2 用一行发布说明记录了一次小而关键的安全修复:在StaticFileHandler的目录重定向逻辑前拦截//开头的请求路径,封堵了由协议相对 URL 引发的开放重定向。通过 tornado/web.py 的修复实现与 tornado/test/web_test.py 的回归测试,我们可以完整还原漏洞成因(前缀、默认文件、路径拼接三要素)、修复策略(前置 403)与验证手段(畸形路径 + 精确断言)。对于所有以 Tornado 提供静态资源的项目,这既是一次具体的版本升级提醒,也是一份关于"重定向目标必须可信"的框架级安全教材。
【免费下载链接】tornadoTornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed.项目地址: https://gitcode.com/gh_mirrors/to/tornado
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考