我们对 RCE 安全公告的回应
我们刚刚发布了一个关于动态 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 表达式,从而允许攻击者执行远程代码。此漏洞已在该软件包的问题跟踪器上报告,但未被安全扫描工具标记。尽管该软件包被广泛使用,但似乎不太可能在上游软件包中修复此问题。即使是为了应对关键安全问题,该软件包将来似乎也不太可能再获得任何维护。为了解决这个问题,我们需要切换到不同的 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 从关键字数组中选择最后一个元素 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/jsonpath 和 https://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 |
空 | 不适用 |
对象的过滤表达式 | $[?(@.key)] |
递归下降后带有通配符的点表示法后的过滤表达式? | $..*[?(@.id>2)] |
递归下降后的过滤表达式? | $..[?(@.id==2)] |
带加法的过滤表达式 | $[?(@.key+50==100)] |
带布尔与运算符和值 false 的过滤表达式 | $[?(@.key>0 && false)] |
带布尔与运算符和值 true 的过滤表达式 | $[?(@.key>0 && true)] |
带布尔或运算符和值 false 的过滤表达式 | $[?(@.key>0 || false)] |
带布尔或运算符和值 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)] |
带路径和路径的等于比较的过滤表达式 | $[?(@.path1 == @.path2)] |
带根引用的等于比较的过滤表达式 | $.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"]) || (@.d == true))] |
带否定和小于的过滤表达式 | $[?(!(@.key<42))] |
带否定且没有值的过滤表达式 | $[?([email protected])] |
非单一存在性测试的过滤表达式 | $[?(@.a.*)] |
带不等于的过滤表达式 | $[?(@.key!=42)] |
带不等于数组或等于 true 的过滤表达式 | $[?((@.d!=["v1","v2"]) || (@.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)] |
求和函数 | $.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] |