跳至主要内容

我们对 RCE 安全漏洞的回应

·阅读时长:12 分钟
chris48s
Shields.io 核心团队

我们刚刚发布了一个关于动态 JSON/TOML/YAML 徽章中的远程代码执行漏洞的关键安全漏洞公告:https://github.com/badges/shields/security/advisories/GHSA-rxvx-x284-4445 感谢 @nickcopi 的帮助。

如果您自行托管 Shields 实例,您应该尽快升级到 server-2024-09-25 或更高版本,以保护您的实例。

这主要是一个针对自行托管用户的安全问题。但是,这也对 shields.io 本身的一些用户产生了一些连锁影响。

1. 授权 Shields.io GitHub OAuth 应用程序的用户

虽然我们已采取措施,在意识到此漏洞后迅速将其关闭,但此攻击向量已在我们的应用程序中存在了一段时间。我们不知道它是否已在 shields.io 上被积极利用。我们也无法证明它没有被利用。

我们不记录或跟踪我们的用户,因此,攻击只会对 shields.io 的最终用户构成非常有限的攻击面。这是出于设计目的。shields.io 持有的(少数)信息资产之一是我们的 GitHub 令牌池。这允许用户通过授权我们的 OAuth 应用程序与我们共享令牌。这样做使我们能够访问一个具有对公共数据的只读访问权限的令牌,我们可以使用该令牌在调用 GitHub API 时增加我们的速率限制。

我们持有的令牌对攻击者没有太大的价值,因为它们只能读取公共数据,但我们无法确定它们是否已被泄露。如果您过去捐赠过令牌并想撤销它,您可以在 https://github.com/settings/applications 撤销 Shields.io OAuth 应用程序,这将使您与我们共享的令牌失效。

2. 动态 JSON/TOML/YAML 徽章的用户

到目前为止,我们一直在使用 https://github.com/dchester/jsonpath 作为我们使用 JSONPath 表达式查询文档的库。 @nickcopi 负责任地向我们报告了该库中的原型污染漏洞如何被利用来构建一个 JSONPath 表达式,使攻击者能够执行远程代码执行。此漏洞已在该软件包的问题跟踪器中报告,但未被安全扫描工具标记。尽管该软件包被广泛使用,但似乎不太可能在 upstream 软件包中修复此漏洞。该软件包似乎也不太可能在未来收到任何进一步的维护,即使是针对严重安全问题的维护。为了解决此问题,我们需要切换到另一个 JSONPath 库。我们决定切换到 https://github.com/JSONPath-Plus/JSONPath,并使用 eval: false 选项禁用脚本表达式。

这是一个重要的安全改进,我们必须做出改变来保护 shields.io 和自行托管应用程序实例的用户安全。但是,从向后兼容性的角度来看,这确实带来了一些权衡。

使用 eval: false

使用 eval: false 的 JSONPath-Plus 会禁用某些依赖于评估 javascript 表达式的查询语法。

例如,以前可以使用类似 $..keywords[(@.length-1)] 的 JSONPath 查询针对 https://github.com/badges/shields/raw/master/package.json 文档,以选择 keywords 数组中的最后一个元素 https://github.com/badges/shields/blob/e237e40ab88b8ad4808caad4f3dae653822aa79a/package.json#L6-L12

这现在不再是一个受支持的查询。

在这种特殊情况下,您可以将该查询改写为 $..keywords[-1:] 并获得相同的结果,但在所有情况下可能都无法做到。我们确实意识到这会移除以前有效的功能,但关闭此远程代码执行漏洞是重中之重,特别是因为在许多情况下都会有变通方法。

实现中的怪癖

从历史上看,每个 JSONPath 实现都有自己的怪癖。虽然大多数简单且常见的查询在不同的实现中会以相同的方式运行,但切换到另一个库意味着一些查询子集可能无法运行或产生不同的结果。

最近 JSONPath 世界发生的一件有趣的事情是 RFC 9535 https://www.rfc-editor.org/rfc/rfc9535,它试图标准化 JSONPath。作为此缓解措施的一部分,我们确实研究了是否可以迁移到符合 RFC9535 的东西。但是,我们评估认为,JavaScript 社区还没有足够成熟/支持的符合 RFC9535 的 JSONPath 实现。这意味着我们正在从一个有怪癖的实现切换到另一个有不同怪癖的实现。

同样,这代表了向后兼容性的不幸中断。但是,必须优先考虑关闭此远程代码执行漏洞,而不是向后兼容性。

虽然我们无法提供精确的迁移指南,但以下列出了 https://github.com/dchester/jsonpathhttps://github.com/JSONPath-Plus/JSONPath 已知与共识实现存在偏差的查询类型。此信息来自出色的 https://cburgmer.github.io/json-path-comparison/ 虽然这是一份很长的清单,但其中许多输入代表的是边缘情况或病态输入,而不是常见用法。

表格
查询类型示例查询
数组切片,其中结束数字很大,步长为负数$[2:-113667776004:-1]
数组切片,其中开始数字和结束数字都很大,步长为负数$[113667776004:2:-1]
数组切片,其中步长为负数$[3:0:-2]
数组切片,其中步长为负数,部分重叠数组$[7:3:-1]
数组切片,其中步长为负数,只有步长$[::-2]
数组切片,其中结束位置为空,步长为负数$[3::-1]
数组切片,其中开始位置为空,步长为负数$[:2:-1]
数组切片,其中范围为 0$[0:0]
数组切片,其中步长为 0$[0:3:0]
数组切片,其中步长包含前导零$[010:024:010]
带空路径的方括号表示法$[]
对象上的带数字的方括号表示法$[0]
字符串上的带数字的方括号表示法$[0]
带数字 -1 的方括号表示法$[-1]
带引号的数组切片字面量的方括号表示法$[':']
带引号的结束方括号字面量的方括号表示法$[']']
带引号的当前对象字面量的方括号表示法$['@']
带引号的转义反斜杠的方括号表示法$['\']
带引号的转义单引号的方括号表示法$[''']
带引号的根字面量的方括号表示法$['$']
带引号的特殊字符组合的方括号表示法$[':@."$,*'\']
带引号的字符串和未转义单引号的方括号表示法$['single'quote']
带引号的联合字面量的方括号表示法$[',']
带引号的通配符字面量 ? 的方括号表示法$['*']
带引号的通配符字面量,位于没有键的对象上的方括号表示法$['*']
带空格的方括号表示法$[ 'a' ]
带两个用点分隔的字面量的方括号表示法$['two'.'some']
带两个用点分隔的字面量的方括号表示法,不带引号$[two.some]
不带引号的方括号表示法$[key]
带点表示法的当前对象@.a
点括号表示法$.['key']
带双引号的点括号表示法$.["key"]
不带引号的点括号表示法$.[key]
带额外点的递归下降后的点表示法 ?$...key
带键的联合后的点表示法$['one','three'].key
带连字符的点表示法$.key-dash
带双引号的点表示法$."key"
带双引号的递归下降后的点表示法 ?$.."key"
带空路径的点表示法$.
数组上名为 length 的键的点表示法$.length
带根字面量的点表示法$.$
带非 ASCII 键的点表示法$.??
带数字的点表示法$.2
带数字 -1 的点表示法$.-1
带单引号的点表示法$.'key'
带单引号的递归下降后的点表示法 ?$..'key'
带单引号和点的点表示法$.'some.key'
带空格填充键的点表示法$. a
标量上递归下降后的带通配符的点表示法 ?$..*
不带点的点表示法$a
不带根的点表示法.key
不带根和点的点表示法key
n/a
对象上的过滤器表达式$[?(@.key)]
带通配符的递归下降后的点表示法后的过滤器表达式 ?$..*[?(@.id>2)]
递归下降后的过滤器表达式 ?$..[?(@.id==2)]
带加法的过滤器表达式$[?(@.key+50==100)]
带布尔 AND 运算符和值为 false 的过滤器表达式$[?(@.key>0 && false)]
带布尔 AND 运算符和值为 true 的过滤器表达式$[?(@.key>0 && true)]
带布尔 OR 运算符和值为 false 的过滤器表达式$[?(@.key>0 || false)]
带布尔 OR 运算符和值为 true 的过滤器表达式$[?(@.key>0 || true)]
带方括号表示法和 -1 的过滤器表达式$[?(@[-1]==2)]
带对象上的数字的方括号表示法的过滤器表达式$[?(@[1]=='b')]
带当前对象的过滤器表达式$[?(@)]
带不同非分组运算符的过滤器表达式$[?(@.a && @.b || @.c)]
带除法的过滤器表达式$[?(@.key/10==5)]
带连字符的点表示法的过滤器表达式$[?(@.key-dash == 'value')]
使用点符号和数字进行筛选表达式$[?(@.2 == 'second')]
使用点符号和数字对数组进行筛选表达式$[?(@.2 == 'third')]
空表达式筛选$[?()]
使用等于号筛选表达式$[?(@.key==42)]
使用等于号对数字数组进行筛选表达式$[?(@==42)]
使用等于号对对象进行筛选表达式$[?(@.key==42)]
使用等于号对数组进行筛选表达式$[?(@.d==["v1","v2"])]
使用等于号对范围为 1 的数组切片进行筛选表达式$[?(@[0:1]==[1])]
使用等于号对带有星号的点符号进行筛选表达式$[?(@.*==[1,2])]
使用等于号数组或等于号 true 进行筛选表达式$[?(@.d==["v1","v2"] || (@.d == true))]
使用带单引号的等于号数组进行筛选表达式$[?(@.d==['v1','v2'])]
使用等于号布尔表达式值进行筛选表达式$[?((@.key<44)==false)]
使用等于号 false 进行筛选表达式$[?(@.key==false)]
使用等于号 null 进行筛选表达式$[?(@.key==null)]
使用等于号数字对范围为 1 的数组切片进行筛选表达式$[?(@[0:1]==1)]
使用等于号数字对带有星号的方括号符号进行筛选表达式$[?(@[*]==2)]
使用等于号数字对带有星号的点符号进行筛选表达式$[?(@.*==2)]
使用带有小数的等于号数字进行筛选表达式$[?(@.key==-0.123e2)]
使用带有前导零的等于号数字进行筛选表达式$[?(@.key==010)]
使用等于号对象进行筛选表达式$[?(@.d=={"k":"v"})]
使用等于号字符串进行筛选表达式$[?(@.key=="value")]
使用带有 unicode 字符转义的等于号字符串进行筛选表达式$[?(@.key=="Mot\u00f6rhead")]
使用等于号 true 进行筛选表达式$[?(@.key==true)]
使用带有路径和路径的等于号进行筛选表达式$[?(@[email protected])]
使用带有根引用符的等于号进行筛选表达式$.items[?(@.key==$.value)]
使用大于号进行筛选表达式$[?(@.key>42)]
使用大于或等于号进行筛选表达式$[?(@.key>=42)]
使用值数组进行筛选表达式$[?(@.d in [2, 3])]
使用当前对象进行筛选表达式$[?(2 in @.d)]
使用 length 自由函数进行筛选表达式$[?(length(@) == 4)]
使用 length 函数进行筛选表达式$[?(@.length() == 4)]
使用 length 属性进行筛选表达式$[?(@.length == 4)]
使用小于号进行筛选表达式$[?(@.key<42)]
使用小于或等于号进行筛选表达式$[?(@.key<=42)]
使用局部点键和数据中的 null 进行筛选表达式$[?(@.key='value')]
使用乘法进行筛选表达式$[?(@.key*2==100)]
使用否定和等于号进行筛选表达式$[?(!(@.key==42))]
使用否定和等于号数组或等于号 true 进行筛选表达式$[?(!(@.d==["v1","v2"]) &#124;&#124; (@.d == true))]
使用否定和小于号进行筛选表达式$[?(!(@.key<42))]
使用否定和无值进行筛选表达式$[?([email protected])]
使用非单一存在性测试进行筛选表达式$[?(@.a.*)]
使用不等于号进行筛选表达式$[?(@.key!=42)]
使用不等于号数组或等于号 true 进行筛选表达式$[?((@.d!=["v1","v2"]) &#124;&#124; (@.d == true))]
使用父轴运算符进行筛选表达式$[*].bookmarks[?(@.page == 45)]^^^
使用正则表达式进行筛选表达式$[?(@.name=~/hello.*/)]
使用成员中的正则表达式进行筛选表达式$[?(@.name=~/@.pattern/)]
使用集合与标量的比较进行筛选表达式$[?(@[*]>=4)]
使用集合与集合的比较进行筛选表达式$.x[?(@[]>=$.y[])]
使用单个等于号进行筛选表达式$[?(@.key=42)]
使用子筛选表达式$[?(@.a[?(@.price>10)])]
使用深度嵌套的子路径进行筛选表达式$[?(@.a.b.c==3)]
使用减法进行筛选表达式$[?(@.key-50==-100)]
使用三重等于号进行筛选表达式$[?(@.key===42)]
使用值进行筛选表达式$[?(@.key)]
使用递归下降后进行筛选表达式$..[?(@.id)]
使用值 false 进行筛选表达式$[?(false)]
使用递归下降后的值进行筛选表达式$[?(@..child)]
使用值 null 进行筛选表达式$[?(null)]
使用值 true 进行筛选表达式$[?(true)]
不使用括号进行筛选表达式$[[email protected]==42]
不使用值进行筛选表达式$[?(@.key)]
函数 sum$.data.sum()
括号符号$(key,more)
递归下降$..
点符号后的递归下降$.key..
标量上的根$
标量上的根 false$
标量上的根 true$
脚本表达式$[(@.length-1)]
从数组中进行重复匹配的联合$[0,0]
从对象中进行重复匹配的联合$['a','a']
使用筛选器进行联合$[?(@.key<3),?(@.key>6)]
使用键进行联合$['key','another']
对没有键的对象使用键进行联合$['missing','key']
在数组切片后使用键进行联合$[:]['c','d']
在方括号符号后使用键进行联合$[0]['c','d']
在带有通配符的点符号后使用键进行联合$.*['c','d']
在递归下降后使用键进行联合$..['c','d']
在带有通配符的点符号后使用重复匹配进行联合$.*[0,:5]
使用切片和数字进行联合$[1:3,4]
使用空格进行联合$[ 0 , 1 ]
使用通配符和数字进行联合$[*,1]