Redis Lua 脚本:修复多个安全漏洞
一个多月前,我收到 Apple Information Security 团队发来的一封电子邮件。在一次审计中,Apple 团队发现 Redis Lua 子系统存在一个安全问题,具体来说是在 cmsgpack 库中。这个库并不是 Lua 本身的一部分,而是我自己编写的 MessagePack 实现。在合并一个改进功能集的 pull request 时,我们引入了一个安全问题。后来,同一团队又在 Lua struct 库中发现了一个新问题;这个库同样不是 Lua 本身的一部分,至少不是我们所使用的 Lua 版本的一部分:我们只是将其源代码嵌入自己的 Lua 实现中,以便为 Redis 用户提供 Lua 解释器的某些功能。随后我又在同一个 struct 包中发现了另一个问题,之后 Alibaba 团队还在 cmsgpack 以及其他使用 Lua API 的代码路径中发现了许多问题。在很短的时间内,我手头已经堆积了一大批与 Lua 相关的漏洞。
这些漏洞主要与在云端提供托管 Redis 服务器这一特定场景有关,因为如果没有直接访问 Redis 服务器的权限,被发现的漏洞几乎不可能被利用:许多 Redis 用户根本不使用 cmsgpack 或 struct 包,即使使用,也几乎不会向它们传入不受信任的输入。然而,对云服务提供商来说情况不同:它们拥有 Redis 实例,有时还是多租户部署,并将这些实例暴露给订阅服务的用户。用户可以向这类 Redis 实例发送任意内容,从而触发漏洞、破坏内存、攻陷 Redis 进程,并可能完全控制 Redis 进程。
例如,下面这个简单的 Python 程序就可以利用 cmsgpack 中的一个漏洞使 Redis 崩溃[1]。
[1] https://gist.github.com/antirez/82445fcbea6d9b19f97014cc6cc79f8a
不过,从能够控制发送到自己实例中的内容的普通 Redis 用户的角度来看,风险仅限于向类似 struct.unpack() 的函数传入不受信任的数据,同时在 format 参数中选择尤其危险的解码格式“bc0”。
协调安全公告
得益于 Apple Information Security 团队、我本人以及各家 Redis 云服务提供商之间的合作和友好沟通,我联系了所有主要的 Redis 服务提供商,努力协调漏洞的发布,以便它们能在漏洞公布前修补自己的系统。我提供了一个统一补丁,使服务提供商能够轻松地将其应用到自己的系统中。最后,在昨天和今天之间,我准备了 Redis 3、4 和 5 的新补丁版本,其中包含安全修复。如果你正在阅读这篇博客文章,那么这些版本都已经发布了。遗憾的是,我没能联系到规模较小或成立较新的云服务提供商。处理与 Redis Labs、Amazon、Alibaba、Microsoft、Google、Heroku、Open Redis 和 Redis Green 的沟通已经耗费了巨大精力,而将信息共享扩展到更多相关方会带来更高的信息泄露风险(每家公司都有许多人参与处理这一过程)。如果你是一家今天才得知这一漏洞的 Redis 服务提供商,我对此表示抱歉;我已经尽力而为。
我想感谢 Apple Information Security 团队以及其他所有服务提供商,为这个问题提供线索和帮助。
Lua 的问题
坦率地说,在设计 Redis Lua 引擎时,我们并没有考虑客户与云服务提供商之间这种安全模型。我们的假设大致是:接触你的 Redis 服务器的人是可以信任的。因此,总体而言,我们没有对 Lua 库进行安全审查。当时的想法是,如果你已经能够访问 Redis API,那么无论如何都可以做出更严重的事情。
然而后来情况发生了变化,云服务提供商限制了向客户开放的 Redis API,从而得以提供托管 Redis 实例。但是,虽然 CONFIG 或 DEBUG 这样的命令可以被禁止,你实际上无法避免开放 EVAL 和 EVALSHA。Redis Lua 脚本是我们社区中使用最广泛的功能之一。
因此,在 Redis 对外暴露和提供的方式发生变化后,Lua 库逐渐在我并未真正注意到的情况下,也变成了安全模型中的一个攻击向量,而这个安全模型本应由 Redis 来处理。正如我所说,在这种模型中,受影响的与其说是 Redis 用户,不如说是托管 Redis 的“云”服务提供商,但无论如何,这是一个必须处理的问题。
针对 Lua 脚本这一具体问题,我们能做些什么来改善云服务提供商当前的安全状况?我确定了几件接下来几个月想做的事。
- Lua 栈保护。Lua 似乎可以通过一种编译方式来确保无法滥用 Lua 栈 API,不过这会带来一定的速度损失。公平地说,我认为 Lua 对栈的假设有些过于简单:Lua 库开发者必须不断检查栈上是否有足够空间来压入新值。处于相同抽象层级的其他语言,其 C API 并不存在这个问题。因此,我会尝试了解在 Lua 的底层 C API 中应用更多保护措施所造成的速度下降是否可以接受;如果可以,我就会实现它。
- 安全审计和模糊测试(fuzz testing)。尽管时间有限,我已经对 Lua struct 库进行了一些模糊测试。我会继续开展这项工作,以检查这一区域是否存在其他错误。我确信还存在更多问题,而我们目前只发现了这一组漏洞,仅仅是因为没有更多时间来调查脚本子系统。因此,这是一项必须开展的重要工作。同样,在这项工作结束时,我会与 Redis 供应商协调,以便它们及时完成修补。
- 从 Redis 用户的角度来看,当有不受信任的数据发送到 Lua 引擎时,使用 HMAC(基于哈希的消息认证码)非常重要,以确保数据未被篡改。例如,有一种流行模式是将用户状态直接存储在用户 cookie 中,之后再对其进行解码。这类数据之后可能会被用作 Redis Lua 函数的输入。在这种情况下,绝对需要使用 HMAC,以确保我们读取的正是之前存储的内容。
- 进一步加强 Lua 沙箱隔离(sandboxing)。关于这个主题,应该已经有大量文献和良好实践。我们已经实现了一些沙箱隔离,但凭借我从事安全工作的经验,我的感觉是,沙箱隔离最终总是一场猫鼠游戏,不可能以完美的方式实现。例如,CPU/内存滥用可能过于复杂,难以达到 Redis 的目标。不过,我们至少应该确保违规行为能够导致一次“优雅”的中止,而不会引发任何内存内容遭到破坏的问题。
- 也许是时候升级 Lua 引擎了?我不确定从安全角度来看,较新的 Lua 版本是否更先进;不过,我们面临一个很大的问题:升级 Lua 可能导致旧脚本不再正常工作。这对 Redis 社区来说是个非常严重的问题,尤其是考虑到 Redis 用户通常开发的脚本类型,更高级的 Lua 版本只能带来有限的用处。
这些问题
已修复的问题列在以下提交中:
- ce17f76b Security: fix redis-cli buffer overflow.
- e89086e0 Security: fix Lua struct package offset handling.
- 5ccb6f7a Security: more cmsgpack fixes by @soloestoy.
- 1eb08bcd Security: update Lua struct package for security.
- 52a00201 Security: fix Lua cmsgpack library stack overflow.
第一条提交与这次工作无关,它修复的是 redis-cli 缓冲区溢出问题;只有在命令行中传入很长的主机参数时,才能利用该问题。其他问题则是我们在 cmsgpack 和 struct 包中发现的问题。
用于复现这些问题的两个脚本如下:
https://gist.github.com/antirez/82445fcbea6d9b19f97014cc6cc79f8a
以及
https://gist.github.com/antirez/bca0ad7a9c60c72e9600c7f720e9d035
两者均由 Apple Information Security 团队编写。不过,第一个脚本经过我修改,以便更可靠地导致崩溃。
受影响的版本
基本上,所有启用了 Lua 脚本的 Redis 都会受到影响。
修复版本以以下 Github 标签的形式提供:
- 3.2.12
- 4.0.10
- 5.0-rc2
稳定版本(4.0.10)也照常可以从 http://download.redis.io 获取。
发布版本的 tarball 哈希值可在这里找到:
https://github.com/antirez/redis-hashes
请注意,发布的版本还包含其他不同的错误修复,因此最好也阅读发布说明,以了解切换到新版本后还会升级哪些其他内容。
希望未来能再写一篇博客文章,介绍计划对 Redis Lua 脚本子系统开展的安全审计。
随机一篇博客