开始更新portswigger2021年度安全文章所有提名列表的技术内容并学习。
————————-
同一个 JSON 文档可以跨微服务解析为不同的值,从而导致各种潜在的安全风险。如果您更喜欢动手实践的方法,尝试https://github.com/BishopFox/json-interop-vuln-labs/,当它们吓到您时,请回来继续阅读。
简介:更多解析器,更多问题
JSON 是 Web 应用程序通信的支柱之一。JSON 的简单性通常被认为是理所当然的,所以我们不将 JSON 解析视为威胁模型的一部分。然而,在我们现代的多语言微服务架构中,我们的应用程序通常依赖于几个单独的 JSON 解析实现,每个都有自己的怪癖。
正如我们通过“HTTP 请求走私”等攻击所看到的那样,解析器之间的差异与多阶段请求处理相结合可能会引入严重的漏洞。在这项研究中,我对 49 个 JSON 解析器进行了调查,对它们的怪癖进行了分类,并展示了各种攻击场景以突出它们的风险(见github)。通过我们的支付处理和用户管理示例,我们将探索 JSON 解析不一致如何掩盖其他良性代码中的严重业务逻辑漏洞。
为什么会出现解析不一致?
(官方和替代规格)
即使在最好的情况下,也不可避免地会出现与规范的细微的、无意的偏差。然而,JSON 解析器还有一些额外的挑战。即使在官方 JSON RFC 中,也有关于一些主题的开放式指南,例如如何处理重复键和表示数字。尽管本指南后面附有关于互操作性的免责声明,但大多数 JSON 解析器用户并不了解这些警告。
解析器之间不一致的一个促成因素是不同的规范:
-
IETF JSON RFC(8259 和更早版本):这是官方的 Internet 工程任务组 (IETF) 规范。
-
ECMAScript 标准:对 JSON 的更改与 RFC 版本同步发布,该标准参考 RFC 以获取有关 JSON 的指南。然而,JavaScript 解释器提供的非规范便利,例如无引号字符串和注释,激发了许多解析器。
-
JSON5:这个超集规范通过显式添加便利特性(例如,注释、替代引号、无引号字符串、尾随逗号)来扩充官方规范。
-
HJSON: HJSON 在精神上与 JSON5 相似,但设计选择不同。
-
……..
那么,为什么一些解析器会开始选择性地合并被忽略的特性,或者采取与解析器行为相矛盾的方法呢?
(开放式指导)
正如以下部分所讨论的,关于处理重复键和表示数字的决定通常是开放式的。我怀疑这可能是由于规范在实现变得流行之后发布的。也许设计人员决定不破坏与预先规范的 JSON 解析器的向后兼容性,包括原始的 JavaScript 实现。
然而,这些决策继续通过生态系统传播到 JSON5 和 HJSON 等超集规范,甚至传播到 BSON、MessagePack 和 CBOR 等二进制变体,我们将在后面讨论。
进一步的互操作性问题来自对数字和字符串编码的延迟指导。在规范的 2017 修订版中,字符串编码仅明确要求为 UTF-8。
有了这个背景,让我们来探讨一下会出现什么问题。
JSON 互操作性(Interoperability)安全风险
我将已识别的互操作性安全风险分为五类:
-
不一致的重复键优先级
-
键冲突:字符截断和注释
-
JSON 序列化怪癖
-
浮点数和整数表示
-
许可解析和其他错误
1.不一致的重复键优先级
Creed said they were different. Creed and Pam were both right, but it depends on whom you ask。
我们中的许多人在我们的开发工作中都遇到过 JSON 的这种怪癖:如果您有重复的密钥会发生什么?
考虑obj["test"]
以下文档中的值:
obj = {"test": 1, "test": 2}
值obj["test"]
是 1 还是 2,还是会产生错误?
根据官方规范,这些结果中的任何一个都是可以接受的。令人惊讶的是,我什至遇到过直接利用重复键优先级来创建自记录 JSON 的开发人员。这是一个例子:
// 对于最后一个键优先的解析器,第一个“测试”键将在解析过程中被忽略
obj = {"test": "this is a description of the test field", "test": "Actual Value"}
您可能会惊讶地发现规范中的指导是描述性的,而不是规定性的。以下来自最新版本的 IETF JSON RFC (8259):
一个名称都是唯一的对象是可互操作的,因为所有接收该对象的软件实现都将就名称-值映射达成一致。当对象中的名称不唯一时,接收此类对象的软件的行为是不可预测的。许多实现只报告姓/值对。其他实现报告错误或无法解析对象,并且一些实现报告所有名称/值对,包括重复项。
观察到 JSON 解析库在它们是否使对象成员的顺序对调用软件可见方面存在差异。其行为不依赖于成员排序的实现将是可互操作的,因为它们不会受到这些差异的影响。
我怀疑规范不想破坏与规范前解析器的向后兼容性。公平地说,注意到了互操作性问题。但在实际意义上,如前所述,有多少开发人员阅读 JSON RFC,或考虑与这种简单格式的互操作性问题?不用说,上述规范中的语言与 RFC 中常见的明确和直接的指导完全不同。
因此,让我们看一些重复键优先级如何出错的示例。
示例:验证代理模式
让我们考虑一个电子商务应用程序,其中我们有一个 Cart 服务,该服务执行业务逻辑,将请求转发到支付服务以进行支付处理,并执行订单履行。让我们尝试免费获得一些东西。此示例将使用下面描述的设计:
假设购物车服务收到这样的请求(注意qty
购物车中第二项的重复键):
POST /cart/checkout HTTP/1.1
...
Content-Type: application/json
{
"orderId": 10,
"paymentInfo": {
//...
},
"shippingInfo": {
//...
},
"cart": [
{
"id": 0,
"qty": 5
},
{
"id": 1,
"qty": -1,
"qty": 1
}
]
}
如下所示,购物车服务在将订单发送到支付服务之前执行业务逻辑。API 是用 Python Flask 编写的,并使用 Python 标准库 JSON 解析器,它使用 last-key 优先级来处理重复的键(对于id: 1
的含义qty = 1
):
@app.route('/cart/checkout', methods=["POST"])
def checkout():
# 1a: Parse JSON body using Python stdlib parser.
data = request.get_json(force=True)
# 1b: Validate constraints using jsonschema: id: 0 <= x <= 10 and qty: >= 1
# See the full source code for the schema
jsonschema.validate(instance=data, schema=schema)
# 2: Process payments
resp = requests.request(method="POST",
url="http://payments:8000/process",
data=request.get_data(),
)
# 3: Print receipt as a response, or produce generic error message
if resp.status_code == 200:
receipt = "Receipt:n"
for item in data["cart"]:
receipt += "{}x {} @ ${}/unitn".format(
item["qty"],
productDB[item["id"]].get("name"),
productDB[item["id"]].get("price")
)
receipt += "nTotal Charged: ${}n".format(resp.json()["total"])
return receipt
return "Error during payment processing"
JSON 正文将成功验证,因为重复的键被忽略,并且所有解析的值都满足约束。现在 JSON 被认为是安全的,原始 JSON 字符串 ( request.get_data()
) 被转发到支付服务。从开发人员的角度来看,当字符串输入随时可用时,为什么要通过重新序列化刚刚解析和验证的 JSON 对象来浪费计算?这个假设应该是合理的。
接下来,在支付服务中接收请求。此 Golang 服务使用高性能的第三方 JSON 解析器 ( buger/jsonparser
)。但是,此 JSON 解析器使用第一键优先级(对于id: 1
的含义qty = -1
)。该服务计算总计,如下所示:
func processPayment(w http.ResponseWriter, r *http.Request) {
var total int64 = 0
data, _ := ioutil.ReadAll(r.Body)
jsonparser.ArrayEach(
data,
func(value []byte, dataType jsonparser.ValueType, offset int, err error) {
// Retrieves first instance of a duplicated key. Including qty = -1
id, _ := jsonparser.GetInt(value, "id")
qty, _ := jsonparser.GetInt(value, "qty")
total = total + productDB[id]["price"].(int64) * qty;
},
"cart")
//... Process payment of value 'total'
// Return value of 'total' to Cart service for receipt generation.
io.WriteString(w, fmt.Sprintf("{"total": %d}", total))
}
购物车服务接收从支付服务收取的总金额,并为响应生成收据。我们从购物车服务查看收据,但我们可以看到一个错误。我们将运送六种价值 700 美元的产品,但我们只收取了 300 美元:
HTTP/1.1 200 OK
...
Content-Type: text/plain
Receipt:
5x Product A @ $100/unit
1x Product B @ $200/unit
Total Charged: $300
然而,在这个例子中,经过验证的 JSON 文档在解析后没有重新字符串化;相反,它使用原始请求中的 JSON 字符串。在第 3 部分JSON 序列化怪癖中,我们将探讨仍然传播风险怪癖的重新字符串化对象的示例。
在实验室 1 中尝试这种攻击方式。https://github.com/BishopFox/json-interop-vuln-labs/tree/master/lab1
2. Key Collision:字符截断和注释
通过 GIPHY
还可以通过字符截断和注释来引发键冲突,从而增加受重复键优先级影响的解析器的数量。
使用字符截断
一些解析器在特定字符出现在字符串中时会截断它们,而另一些则不会。这可能导致不同的键在解析器子集中被解释为重复项。例如,以下文档在某些 last-key 优先级解析器中似乎具有重复键,但在其他文档中则没有:
{"test": 1, "test[raw x0d byte]": 2}
{"test": 1, "testud800": 2}
{"test": 1, "test"": 2}
{"test": 1, "test": 2}
对于多轮反序列化和重新序列化,这些字符串表示通常不稳定。例如,Unicode 代码点是不成对U+D800
的U+DFFF
UTF-16 代理,虽然不成对的代理可以编码为 UTF-8 字节字符串,但它被认为是非法的 Unicode。
所有这些示例都可以以与前一个示例和实验 1 类似的方式使用。但是,允许对非法 Unicode 进行编码和解码的环境(例如 Python 2.x)可能容易受到需要存储(序列化)的复杂攻击并检索(反序列化)这些值。
让我们从观察 Python 2.x 中的 Unicode 编码和解码行为开始:
$ python2
>>> import json
>>> import ujson
# Serialization into illegal unicode.
>>> u"asdfud800".encode("utf-8")
'asdfxedxa0x80'
# Reserializing illegal unicode
>>> json.dumps({"test": "asdfxedxa0x80"})
'{"test": "asdf\ud800"}'
# Let's observe the third party parser ujson's truncation behavior and how it creates a duplicate key.
>>> ujson.loads('{"test": 1, "test\ud800": 2}')
{u'test': 2}
正如我们将在下一个示例中看到的那样,攻击者可以使用此功能绕过清理检查,例如,创建和存储一个名为superadminud888的角色,该角色可能会被检索和解析为superadmin
. 但是,这种技术需要支持编码和解码非法 Unicode 代码点(不那么难),以及具有不会抛出异常的类型系统的数据库(更难)。
在接下来的实验中,我们将使用 Python 2.7 和 MySQL 以二进制模式来让我们专注于存储非法 unicode 的风险及其对不一致 JSON 解析的影响。
示例:验证存储模式
让我们考虑一个多租户应用程序,其中组织管理员能够创建自定义用户角色。此外,我们知道具有跨组织访问权限的用户被分配了内部角色superadmin
。让我们尝试提升权限。此示例将使用如下所示的设计:
首先,让我们尝试强制创建具有superadmin
权限的用户:
POST /user/create HTTP/1.1
...
Content-Type: application/json
{
"user": "exampleUser",
"roles": [
"superadmin"
]
}
HTTP/1.1 401 Not Authorized
...
Content-Type: application/json
{"Error": "Assignment of internal role 'superadmin' is forbidden"}
如上所示,User API 有一个服务器端的安全控制来阻止用户创建具有该superadmin
角色的新用户。此控件由 Roles API 共享,以避免覆盖现有的用户定义和系统角色。在这里,我们假设User API 上/user/
的/role/
端点使用行为良好、兼容的解析器。
相反,为了影响下游解析器,我们将创建一个名称在解析器中不稳定的角色,superadminud888
:
POST /role/create HTTP/1.1
...
Content-Type: application/json
{
"name": "superadminud888"
}
HTTP/1.1 200 OK
...
Content-type: application/json
{"result": "OK: Created role 'superadminud888'"}
接下来,我们使用新的用户定义角色创建一个用户:
POST /user/create HTTP/1.1
...
Content-Type: application/json
{
"user": "exampleUser",
"roles": [
"superadminud888"
]
}
HTTP/1.1 200 OK
...
Content-Type: application/json
{"result": "OK: Created user 'exampleUser'"}
用户 API 将用户存储到数据库中。到目前为止,所有解析器都将用户定义的角色 ( superadminud888
) 视为与内部角色不同的名称superadmin
。
但是,当稍后访问跨组织/admin
端点时,服务器会从 Permissions API 请求用户的权限。Permissions API 忠实地对角色进行编码,如下所示:
GET /permissions/exampleUser HTTP/1.1
...
HTTP/1.1 200 OK
...
Content-type: application/json
{
"roles": [
"superadminud888"
]
}
但这里出了问题:管理 API 使用了第三方ujson
解析器。正如我们之前看到的,这个解析器会截断任何包含非法代码点的字节:
@app.route('/admin')
def admin():
username = request.cookies.get("username")
if not username:
return {"Error": "Specify username in Cookie"}
username = urllib.quote(os.path.basename(username))
url = "http://permissions:5000/permissions/{}".format(username)
resp = requests.request(method="GET", url=url)
# "superadminud888" will be simplified to "superadmin"
ret = ujson.loads(resp.text)
if resp.status_code == 200:
if "superadmin" in ret["roles"]:
return {"OK": "Superadmin Access granted"}
else:
e = u"Access denied. User has following roles: {}".format(ret["roles"])
return {"Error": e}, 401
else:
return {"Error": ret["Error"]}, 500
如上所示,我们的用户定义角色将被截断为superadmin
,授予对特权 API 的访问权限。
在实验 2 中试用此示例。https://github.com/BishopFox/json-interop-vuln-labs/tree/master/lab2
使用注释截断
许多 JSON 库支持 JavaScript 解释器环境中的无引号字符串和注释语法(例如/*
, */
)。但是,这些功能都不是官方规范的一部分。这些功能允许解析器处理如下文档:
obj = {"test": valWithoutQuotes, keyWithoutQuotes: "test" /* Comment support */}
给定两个支持无引号字符串的解析器,但只有一个能够识别注释,我们可以走私重复键。考虑下面的例子:
obj = {"description": "Duplicate with comments", "test": 2, "extra": /*, "test": 1, "extra2": */}
在这里,我们将使用每个解析器的序列化程序来查看其各自的输出。
序列化器 1(例如,GoLang 的 GoJay 库)将产生:
-
description = "Duplicate with comments"
-
test = 2
-
extra = ""
序列化器 2(例如,Java 的 JSON 迭代器库)将产生:
-
description = "Duplicate with comments"
-
extra = "/*"
-
extra2 = "*/"
-
test = 1
或者,直接使用注释也可以是有效的:
obj = {"description": "Comment support", "test": 1, "extra": "a"/*, "test": 2, "extra2": "b"*/}
以下是 Java 的 GSON 库执行的解码:
{"description":"Comment support","test":1,"extra":"a"}
这是 Ruby 的 simdjson 库执行的解码:
{"description":"Comment support","test":2,"extra":"a","extra2":"b"}
这展示了额外解析功能的不一致性如何导致独特的密钥冲突攻击。
3. JSON 序列化怪癖
到目前为止,我们只关注 JSON 解码,但几乎所有实现都提供 JSON 编码(也称为序列化)。让我们看几个例子。
不一致的优先级:反序列化与序列化
传统的智慧是避免重复键,这对于内部服务很容易做到,但对于外部用户输入则无法保证。因此,并非所有解析器都使用重复键来探索行为。在一个实例中,解析器(Java 的 JSON 迭代器)产生以下输入和输出:
输入:
obj = {"test": 1, "test": 2}
输出:
obj["test"] // 1
obj.toString() // {"test": 2}
如上所示,key检索和序列化的值不同。底层数据结构似乎保留了重复键的值;但是,序列化器和反序列化器之间的优先级不一致。
使用重复键生成文档
根据规范,序列化重复键是可以接受的,一些解析器(例如,C++ 的 rapidjson)就是这样做的:
输入:
obj = {"test": 1, "test": 2}
输出:
obj["test"] // 2
obj.toString() // {"test": 1, "test": 2}
在这些情况下,重新序列化解析的 JSON 对象不会提供保护。这些序列化行为允许攻击者跨清理层走私值。正如我们之前所见,这可能导致业务逻辑缺陷、注入漏洞或其他安全影响。
4. 浮点数和整数表示
现在我们已经观察到重复键的许多风险,我们将检查数字表示。首先,让我们看一下讨论号码互操作性的 RFC 节选:
由于实现 IEEE 754 binary64(双精度)数字 [IEEE754] 的软件普遍可用并被广泛使用,因此可以通过不期望比这些提供的精度或范围更高的实现来实现良好的互操作性,在某种意义上,实现将近似于其中的 JSON 数字预期的精度。JSON 数字(例如 1E400 或 3.141592653589793238462643383279)可能表示潜在的互操作性问题,因为它表明创建它的软件期望接收软件具有比广泛可用的更大的数字量级和精度功能。
请注意,当使用此类软件时,在 [-(2**53)+1, (2**53)-1] 范围内的整数数字是可互操作的,因为实现将完全同意它们的数字价值观。
让我们从查看大数开始。
不一致的大数解码
如果解码不准确,大量数字可能被解码为MAX_INT
或0
(或MIN_INT
当我们接近负无穷大时)。跨多个解析器,我们会发现数量很大,比如:
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
可以解码为多种表示形式,包括:
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
9.999999999999999e95
1E+96
0
9223372036854775807
我们将尝试一个例子。
示例:不一致的大数解码
让我们重温一下 实验 1。我们知道 Payments API 中使用的第三方 Golang jsonparser 库会将大数字解码为 0,而 Cart API 会忠实地解码数字。我们可以利用这种不一致来获得免费物品。让我们购买大量的电子礼品卡(id: 8
):
要求:
POST /cart/checkout HTTP/1.1
...
Content-Type: application/json
{
"orderId": 10,
"paymentInfo": {
//...
},
"shippingInfo": {
//...
},
"cart": [
{
"id": 8,
"qty": 999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999
}
]
}
回复:
HTTP/1.1 200 OK
...
Content-Type: text/plain
Receipt:
999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999x $100 E-Gift Card @ $100/unit
Total Charged: $0
业务逻辑层忠实地解码整数,而支付处理层默认为大数的值 0。
在实验 1 中尝试这种攻击:第 2 部分,请求在lab1_alt_req.json
. 如实验室所述,jsonparser 库可以通过适当的错误检查来检测此溢出。
具有无穷大的不一致类型表示
官方 RFC 不支持正无穷和负无穷以及 NaN(不是数字)。但是许多解析器选择了变通方法。反序列化和/或重新序列化值可能会导致各种结果,例如:
输入:
{"description": "Big float", "test": 1.0e4096}
输出:
{"description":"Big float","test":1.0e4096}
{"description":"Big float","test":Infinity}
{"description":"Big float","test":"+Infinity"}
{"description":"Big float","test":null}
{"description":"Big float","test":Inf}
{"description":"Big float","test":3.0e14159265358979323846}
{"description":"Big float","test":9.218868437227405E+18}
请注意从 JSON 数字到字符串的类型转换。严格比较中的类型转换可能是良性的,但松散比较中可能导致类型杂耍漏洞。考虑以下代码(注意:字符串被解释为 0):
<b><?php</b>
echo 0 == 1.0e4096 ? "True": "False" . "n"; # False
echo 0 == "Infinity" ? "True": "False" . "n"; # True
<b>?></b>
与前面的示例一样,业务逻辑层可能会错误地验证解码不一致的值。更喜欢在使用前进行严格比较或执行类型验证。
5. 许可解析和一次性错误
一些解析器允许文档中的杂散字符、替代引号字符和语法错误,而另一些则严格执行 RFC 定义的语法。让我们看看与重复键无关的许可解析实例。
尾随垃圾
允许尾随垃圾是许多 JSON 解析器的一个众所周知的问题,多年来一直被滥用以进行跨站点请求伪造 (CSRF) 攻击。为了绕过同源策略(SOP)“简单请求”限制,可以发布带有尾随等号的 JSON 文档来建议x-form-urlencoded
文档,这在跨域请求中是允许的。例如:
POST / HTTP/1.1
...
Content-Type: application/x-www-form-urlencoded
{"test": 1}=
忽略Content-Type
和处理所有请求为 JSON 的服务将暴露于这些类型的 CSRF 攻击。
拒绝服务:分段错误
两个解析库在格式错误的 JSON 上崩溃。这两种情况都已报告给维护者。受影响的解析器的名称将在未来修复后更新。
也就是说,出于性能的考虑,许多解析器依赖于可能容易受到内存安全问题影响的低级例程。请务必留意依赖本机代码的解析器。加入像 Google 的 OSS-Fuzz 这样的程序是轻松访问模糊测试的好方法。
关于二进制 JSON 解析器
我简要测试了 BSON、MessagePack、UBJSON 和 CBOR 格式,以及它们各自解析器的示例。这些解析器也存在许多相同的问题。尽管一些序列化程序拒绝创建多个密钥的二进制表示,但我们可以通过字节交换手动创建恶意文档,如下 MessagePack 所示:
json_doc = {u'test': 1, u'four': 2} # use an ABI-compatible string to replace (e.g., 'four')
encoded = msgpack.packb(json_doc, use_bin_type=True)
encoded = encoded.replace(b'four', b'test')
接下来,我们使用 Dot Net MessagePack 反序列化器处理此文档并使用包含的 JSON 序列化器:
Console.WriteLine(MessagePackSerializer.ConvertToJson(File.ReadAllBytes("msgpack.bin")));
这产生了以下输出:
{"test":1,"test":2}
如果我们查看文档,许多规范都试图向后兼容 JSON。也就是说,像 BSON 这样的规范确实试图更明确地说明如何处理重复键等情况。
肯定有更多机会发现这些二进制格式的进一步互操作性问题。
结果:解析器行为
该调查由以下语言的 49 个解析器组成,代表标准库解析器(如果可用)和第三方解析器:
-
C/C++
-
C#
-
Elixir/Erlang
-
Go
-
Java
-
JavaScript
-
PHP
-
Python
-
Ruby
-
Rust
(有关解析器和版本号的完整列表,请参阅附录 A。)
下面的结果定义了具有不常见行为的解析器。这些代表与规范和/或官方规范的偏差。其中一些行为是经过深思熟虑的设计选择,由各自的超规范(例如 JSON5)定义。
除了分段错误之外,这些行为在单个解析器的上下文中是无害的,这可以防止它们开始被归类为特定解析器的漏洞。然而,正如我们所观察到的,这种行为可能会通过互操作性引入安全风险。因此,以下行为在未来可能会或可能不会改变:
重复键的第一键优先级:
-
Go[jsonparser]
-
Go[gojay]
-
C++[rapidjson]
-
Java[json-iterator]
-
Elixir[Jason]
-
Elixir[Poison]
-
Erlang[jsone]
字符截断:
-
通过不成对的代理发生冲突
-
Python[ujson]
-
PHP [json5]
-
通过反斜杠后跟回车字节 (0x0d) 的冲突
-
Rust[json5]
-
PHP[json5]
-
通过杂散引号发生冲突
-
Ruby[simdjson]
-
通过杂散反斜杠碰撞 { “test”: 2, “test”: 1}
-
C#[Jayrock.json]
-
Ruby[stdlib/ext]
-
Ruby[stdlib/pure]
-
JavaScript[json5]
-
Rust[json5]
拒绝服务(分段错误):
-
(Two instances) To be announced following remediation
评论截断:
-
Java[json-iterator]
-
Ruby[simdjson]
字符串化的重复键:
-
C++[rapidjson]
-
C#[MessagePack] (v2.2.85)
意外评论支持(非 JSON5/HJSON 解析器):
-
Ruby[stdlib/ext]
-
Ruby[stdlib/pure]
-
Ruby[oj]
-
Ruby[Yajl]
-
Go[jsonparser]
-
Go[gojay]
-
Java[GSON]
-
Java[Genson]
-
Java[fastjson]
-
C#[Newtonsoft.Json]
-
C#[Utf8Json]
-
C#[Jayrock.Json]
-
C#[Manatee.Json]
大数(转换为字符串或“Infinity”):
-
Python[stdlib/json] – In: 1.0e4096, Out: “Infinity”
-
Python[ujson] In: 1.0e4096, Out: “Inf”
-
Java[Jackson] – In: 1.0e4096, Out: “Infinity”
-
Java[Genson] – In: 1.0e4096, Out: “Infinity”
-
Java[Jodd] – In: 1.0e4096, Out: “+Infinity”
-
C#[Manatee] – In: 1.0e4096, Out: “Infinity”
-
C#[Newtonsoft.Json] – In: [9 repeated 96 times] Out: “[9 repeated 96 times]”
大数(重要四舍五入):
-
Ruby[oj] – In: 1.0e4096, Out: 3.0e14159265358979323846
-
C#[Utf8Json] – In: 1.0e4096, Out: 9.218868437227405E+18
-
PHP[jsonlint] – In: [9 repeated 96 times] Out:9223372036854775807
大数(不检查错误返回 0):
-
Go[jsonparser] – In: 1.0e4096, Out: 0,
总的来说,我发现,在接受调查的 49 个 JSON 解析器中,每种语言都至少有一个解析器表现出一种具有潜在风险的互操作性行为。标准库提供的解析器往往是最合规的,但它们往往缺乏速度,这在微服务架构中越来越重要。这促使开发人员选择性能更高的第三方解析器。
补救措施:我们如何降低互操作性风险?
当 JSON 只是 ECMAScript 标准的一部分时,很难知道它会变得多么普遍。从这个 RFC 和上面讨论的互操作性错误中可以学到一些重要的经验教训。我在下面概述了针对不同受众的补救和测试指南:
对于 JSON 解析器维护者:
-
在重复键上生成致命的解析错误。
-
不要执行字符截断。相反,用占位符字符替换无效的 Unicode(例如,不成对的代理应显示为 Unicode 替换字符 U+FFFD)。截断可能会破坏多解析器应用程序的清理例程。
-
避免偏离所选规范,或提供符合 RFC 8259(或相关)定义的“严格”模式。
-
处理无法忠实表示的整数或浮点数时会产生错误。
对于软件工程师:
-
盘点整个架构中的现有解析器。使用提供的测试用例、可用文档或针对上述现有行为列表来识别解析器中的行为差距。
对于安全人员:
-
这些是细微的攻击,不容易从外部识别。如果您有权访问源代码,请寻找具有已知怪癖的解析器。尝试复制密钥并使用实验室自述文件中的建议来尝试引发冲突(#attack-techniques)。
关于:JSON 模式验证器?
JSON Schema 规范可以帮助简化和强制执行类型安全和约束,但它不能帮助处理重复键。JSON Schema 实现本身不执行 JSON 解析,而是仅处理解析的对象。为了证明这一点,我在这篇博文附带的易受攻击的实验室中使用了 JSON Schema。
例如,Java 的 JSON Schema 实现需要org.json.JSONObject
输入,而 Python 的 json-schema 库依赖于 Python Dictionary 对象,如下所示:
import jsonschema
schema = {
"type": "object",
"properties": {
"test": {
"type": "integer",
"minimum": 0,
"maximum": 100,
}
}
}
jsonschema.validate(instance={"test": 1}, schema=schema)
JSON Schema 可能有助于减轻一些解析风险,例如类型检查和限制允许的整数范围。但是不一致的解析会成为 JSON Schema 的盲点。
JSON LINTERS 呢?
JSON linter 使用字符串输入,通常用于 IDE 或编程环境。Linter 只是通常不返回解码对象的解析器。理想情况下,我们希望使用我们的解码解析器进行一致的解析。
话外:
解析差异将继续成为微服务安全的主题。尽管检测这些攻击的条件可能具有挑战性,但实验室表明,即使像 JSON 这样看似良性的标准也会引入意想不到的互操作性怪癖,从而在更复杂的系统中导致严重的安全问题。
最后,在设计协议或标准时,将行为限制为确定性结果不仅可以提高互操作性,还可以更轻松地报告错误和改进我们的软件。通过定义以前未定义的行为来破坏现有标准中的向后兼容性可能会导致回退。但是在微服务架构的现代环境中,互操作性变得越来越复杂,这可能是一个值得的选择。
附录 A – Version Numbers
-
C/C++
-
tencent/rapidjson 1.1.
-
nholmann/json 3.9.1
-
C#
-
System.Text.Json (Runtime 5.0.1
-
Newtonsoft.Json 12.0.3
-
Utf8Json 1.3.7
-
Jayrock.Json 0.9.16530.1
-
Manatee.Json 13.0.4
-
Elixir/Erlang
-
jason 1.2
-
poison 4.0
-
jiffy 1.0
-
json 1.4
-
jsone 1.5
-
jsx 3.0
-
Go
-
encoding/json (go1.14.7)
-
buger/jsonparser (cb835d480ac58e1b4be76afeac49e89ed651c3b5 Jul 30 2020)
-
francoispqt/gojay (v1.2.13)
-
Java
-
org.json 20201115
-
Jackson-databind 2.12.0
-
Genson 1.6
-
Jodd 6.0.2
-
fastjson 1.2.73
-
json-iterator 0.9.23
-
JavaScript
-
JSON (stdlib)
-
json5 2.1.3
-
json-buffer 3.0.1
-
buffer-json 2.0.0
-
parse-json 1.3.1
-
secure-json-parse 2.1.0
-
PHP
-
json_decode (stdlib)
-
colinodell/json5 2.1.0
-
seld/jsonlint 1.8.2
-
halaxa/json-machine 0.3.3
-
salsify/jsonstreamingparser 8.2.0
-
Python 3
-
json (stdlib)
-
orjson 3.4.6
-
pysimdjson 3.1.1
-
rapidjson 1.0
-
simplejson 3.17.2
-
ujson 4.0.1
-
Ruby
-
JSON ext (stdlib; Ruby 2.7.2p137)
-
JSON pure (stdlib; Ruby 2.7.2p137)
-
json5 0.0.1
-
oj 3.10.18
-
yajl-ruby 1.4.1
-
simdjson 1.0.1
-
Rust
-
serde_json 1.0.60
-
json 0.12.4
-
json5 0.3.0
-
serde-hjson 0.9.1
原文始发于微信公众号(军机故阁):JSON Interoperability 漏洞探索