ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JSON注入不是独立漏洞:JSON为载体的注入攻击类型与防御实践

JSON注入不是独立漏洞:JSON为载体的注入攻击类型与防御实践 先说我的结论json注入不是一种独立漏洞类型而是一组注入攻击在JSON承载之下的变体。很多开发同学有这样的误解——接口请求体是JSON格式这么严格怎么可能被注入直到线上日志里出现一条带单引号的异常参数或者内部安全测试发现某个“看起来人畜无害”的字符串字段直接拼进了SQL才意识到问题没有那么简单。这篇文章我想把“json注入”这件事讲透包括它最常见的出现位置、为什么自动化扫描器会漏掉、除了SQL之外还有哪些注入路径、以及防御侧真正能落地的手段。适合写给三类人后端接口开发、刚开始接触Web安全的人、以及打CTF/靶场时需要系统理解注入原理的选手。1. 一个真实的“JSON注入”现场格式安全不等于数据安全1.1 从一段后端代码说起很多后端服务的写法是这样的const body JSON.parse(event.body); const sql SELECT * FROM users WHERE username ${body.username};如果你写过类似代码下面这段一定要看完。JSON.parse本身没有任何问题问题出在“从解析结果到危险函数中间没有任何校验”。JSON是一种序列化格式它规定数据的骨架是{}、[]、引号、冒号、逗号它也规定了值的类型有字符串、数字、布尔、null、数组和对象。但它不约束值的内容。也就是说username: jack合法username: OR 11在JSON格式里也同样合法。用个生活化的类比快递面单的填写格式是标准的但面单上写什么内容、包裹里装什么东西不会因为“面单格式正确”就自动安全。后端拿到JSON里的字符串就像收到一个包裹拆开后能不能直接丢进炉子里烧取决于你有没有检查而不是取决于包裹外面那层面单是否规范。1.2 注入的本质数据被当成指令执行再往底层看一步。所有注入类漏洞的根因都是用户输入的数据被拼接进了控制平面也就是数据流跑到了指令流的位置上。SQL注入字符串被拼进了SQL语句数据库解释器把参数内容当成了SQL的一部分命令注入字符串被拼进了Shell命令命令解释器把参数内容当成了命令的一部分模板注入字符串被拼进了模板引擎渲染引擎把参数内容当成了模板语法原型污染字符串键触发了对象原型链的写入运行时把参数内容当成了对象结构的修改指令。JSON在整个链路里扮演的角色是什么是信封是传输容器。服务端解析完JSON后拿到的仍然是普通字符串、数字、布尔。这些值本身没有“安全”或“危险”的属性关键在于它们流向了哪个解释器。所以“json注入”并不存在于JSON解析阶段它存在于“JSON数据被取出来之后、被使用的方式”上。理解了这一点你就不会再问“JSON格式到底安不安全”因为你该关心的是“JSON里的字段到底去了哪里”。1.3 JSON注入与常见注入类型的关系为了后面讨论方便先把主流的“借JSON入口”的攻击类型列一张表注入类型在JSON请求中的常见触发方式核心问题SQL注入字符串或数字字段被拼入SQL语句数据库解释器被注入指令命令注入字符串字段被拼入Shell命令拼接命令解释器被注入指令XXE注入JSON处理后又拼装XML被解析外部实体XML解析器加载了外部实体原型污染__proto__、constructor等键被递归合并对象原型链被篡改也就是说广义上的“json注入”不是一个CWE编号而是一个入口描述。实际测试和防御时要顺着“JSON字段 - 具体解释器”这个链路去逐个排查。2. 藏在JSON字段里的SQL注入为什么扫描器容易看走眼2.1 靶场里的经典形态数字型和字符型如果你在Pikachu或DVWA这类靶场里练过SQL注入应该对“数字型”和“字符型”这两个概念很熟。它们在JSON请求体里的表现方式略有不同但原理一致。所谓数字型是指后端把JSON里的某个数值字段直接放进SQL比如SELECT * FROM products WHERE id 100如果这里用的是字符串拼接而不是参数化那么当这个id字段来自JSON的id: 100时攻击者可以把100改成100 OR 11只是注意格式上要从JSON数字变成JSON字符串很多扫描器在这一步就放弃了。字符型则是把JSON字符串字段放进SQL时字段被单引号包裹SELECT * FROM users WHERE username jack这种场景下注入的关键在于引号闭合和后续的注释截断。比如输入值里自带一个就可能让原本的SQL语句结构发生改变。很多初学者有个错误认知把请求从form表单改成JSON body就“更安全”了。其实后端一旦拿body.username去拼接SQL改什么都没用。JSON只是换了个箱子箱子里装的东西还是一样的。2.2 自动化扫描器为什么会漏我见过不少团队安全测试依赖全自动扫描器结果报告全绿实际手工一测就发现问题。扫描器在JSON接口上的漏报率比在传统GET参数上高很多原因主要有三个。第一个原因扫描器默认要测的是query参数和form表单参数。很多开源扫描器拿到一个请求后优先从URL和表单里提取参数JSON body需要额外配置请求模板不然根本不会探测。第二个原因是转义问题。JSON对引号有严格要求如果扫描器插入的payload里带了未转义的双引号整个JSON体在服务端解析阶段就失败了请求直接返回400。业务逻辑根本没执行到扫描器自然得到一个“不存在漏洞”的假阴性。第三个原因更隐蔽即使请求能正常到达业务逻辑很多扫描器的检测规则只盯着“有没有数据库报错回显”但生产环境的接口通常统一屏蔽了报错信息这时候扫描器判断不了也就直接跳过了。所以结论很直接JSON接口的安全测试不能只靠自动化扫描器必须带着手工分析的思路走一遍。2.3 手工确认和上报的思路这里补充一套适用于授权靶场和本地实验环境的判断流程也是防御侧定位漏洞的通用方法。第一步抓包观察请求体的JSON结构确定哪些字段会被后端取用。字段名本身可能就暴露了用途比如username大概率进查询条件sort可能进ORDER BYpage可能进LIMIT。第二步对疑似进SQL的数值字段做异常输入测试。把数字1改成带空格的字符串改成浮点数改成带引号的字符串观察响应差异。如果某个输入导致接口返回500、SQL报错、或者返回的数据集明显异常就说明这个字段到达了数据库查询层。第三步对字符型字段测试引号和注释符。当输入包含单引号时接口行为发生突变说明字段被拼进了单引号包裹的SQL判断里。这类判断不需要复杂的payload只需要观察“输入一个引号后报不报错”就够了。第四步把触发点、触发条件、影响范围、修复建议一起写进测试报告。看到这里你应该有个感觉这套流程与其说是“攻击技巧”不如说是“代码审计辅助手段”。目的是找到问题并修复而不是打穿系统。3. 不止SQLJSON请求体还能牵出XXE、命令执行和原型污染3.1 XXEJSON接口怎么会被XML实体影响这个问题听起来有点绕——JSON接口和XML有什么关系关键在于很多后端系统不是只处理一种格式。举例前端传递JSON给网关网关解析后生成一段XML报文转发给下游老系统或者一个对象是application/json但内部某个字段的值是XML代码把它拼进了一个XML文档再交给解析器。如果下游用的是libxml之类支持外部实体的XML解析库且实体加载没有被禁掉那么当JSON字段里包含用户可控的内容并进入XML解析时就可能触发XXE类型的注入。这类问题的核心点有三个动态拼接XML、允许外部实体、解析器配置未关闭实体加载。防御也很明确第一不要用字符串拼接去生成XML尽量用XML序列化库第二确实无法避免时把XML解析器的外部实体、DTD加载全部禁用第三对接收的JSON做Schema校验拒绝包含、DOCTYPE、ENTITY这类特征值的字段。3.2 命令注入JSON字段到达Shell前的“最后一公里”命令注入在JSON接口里出现得没有SQL注入频繁但一旦出现危害往往是服务器直接被控制。它和SQL注入的共同点是“拼接”。后端代码如果做了类似这样的操作import subprocess filename json_body.get(filename) result subprocess.run(fcat {filename}, shellTrue, capture_outputTrue)那filename这个来自JSON的字符串就直接进入了Shell。攻击者不需要写什么复杂的payload只要在文件名里带上管道符、分号这类Shell元字符就可能改变整条命令的结构。防御命令注入有几个层级。最优先的是尽量避免shellTrue使用参数数组的形式执行命令让文件名作为参数传递而不是命令文本的一部分其次是白名单只允许固定目录下的固定文件名最后是运行用户权限最小化即使命令被注入也无法读取核心敏感文件。我在实际项目中见过因为一个异步任务接口没加固JSON里的cmd字段直接被拼进os.system()的情况。那种问题的第一责任人不是安全团队而是写这段“图省事”代码的人。所以每次看到os.system、exec、cmd这样的写法都要下意识问一句这里的参数来自哪里3.3 原型污染藏在“proto”里的逻辑后门原型污染不是传统意义上的“注入”但它确实是JSON.parse相关的高发安全问题值得单拿出来讲。JavaScript的JSON.parse会把JSON里的所有键原样解析成对象属性包括__proto__和constructor这种特殊键。如果代码后续做了递归合并、深拷贝、配置覆盖之类的操作就可能把这些键写进对象的原型链导致所有对象实例共享被污染的属性。举个防御侧的例子合并用户输入的配置时function merge(target, source) { for (const key of Object.keys(source)) { if (typeof source[key] object source[key] ! null) { merge(target[key], source[key]); } else { target[key] source[key]; } } }如果source来自JSON.parse的用户输入里面带有__proto__: {isAdmin: true}这个合并逻辑就可能把isAdmin写到Object.prototype上导致某些权限判断直接失效。防御方案有三类解析后递归删除__proto__、constructor、prototype这些危险键合并操作改用Object.assign配合白名单键或者使用不可变数据工具最稳妥的是在JSON Schema层直接禁止这些字段出现。这里的经验是不要相信任何“递归合并”逻辑的安全性尤其当合并对象来自用户可控的JSON时宁可多写几行白名单校验也不要图通用性。4. 一套能落地的防御链路从Schema校验到参数化查询4.1 入参校验用JSON Schema把脏数据挡在业务外防御的第一道门在API入口。对JSON请求体做Schema校验是我在所有项目里最推荐优先做的事性价比极高。一个典型的JSON Schema长这样{ type: object, properties: { username: { type: string, maxLength: 32, pattern: ^[a-zA-Z0-9_]$ }, id: { type: integer, minimum: 1 } }, additionalProperties: false }这里有几个关键设计值得说明。pattern限制了字符串的字符集比如用户名只允许字母数字下划线这样即使攻击者想塞引号、括号、管道符也在入口就被拦截了。maxLength限制长度主要是防止超长载荷绕过部分框架的限制。additionalProperties:false会拒绝所有未声明的字段也就是把__proto__、constructor这类隐藏键直接挡在门外了。注意Schema校验不是万能钥匙。它只能挡住“不符合格式”的数据但挡不住“符合格式但有恶意含义”的数据。比如一个枚举值为[1,2,3]的字段传1 OR 11会被pattern挡住但一个内容为admin--的评论字段格式上完全合法仍可能进入后端拼接点。所以Schema校验是第一道门绝不是唯一一道门。4.2 查询构造参数化才是根治手段无论前面加了多少校验最根本的防线仍然是参数化查询。它的原理是让数据和SQL结构彻底分离数据库不再把参数内容当作SQL指令去解析。以Python为例cursor.execute(SELECT * FROM users WHERE username %s, (username,))以Java为例PreparedStatement stmt conn.prepareStatement(SELECT * FROM users WHERE username ?); stmt.setString(1, username);以Node.js的mysql库为例connection.query(SELECT * FROM users WHERE username ?, [username], callback);参数化的好处在于无论username这个变量里带了多少引号、注释符、连字符在数据库看来它永远只是一个“值”不会改变SQL的结构。同样的道理也适用于命令注入场景。能用数组参数形式执行命令就不要拼shellTrue的字符串。能用白名单限定文件名就不要接受直接传路径。原则永远是别让用户可控的数据拥有语法能力。4.3 反序列化与依赖安全JSON解析本身的风险相对可控但围绕JSON的周边操作需要额外注意几点。第一递归合并。前面提到的原型污染本质是递归合并逻辑默认信任了输入。如果项目里有“合并配置对象”这种功能必须处理危险键或者直接用JSON.parse后的白名单提取。第二依赖版本。如果你的技术栈里有XML解析库尤其是老版本libxml或Java自带的XML解析建议确认一下外部实体是否默认开启。很多CVE的触发条件就是解析器默认配置不安全。这不是JSON的问题但它会顺着JSON字段的拼装进入攻击面。第三运行权限。容器和进程尽量用最小权限运行不跑root不挂载多余目录。这样最坏情况下注入成功了攻击者能拿到的也有限。附一个我常用的自查表自查点建议JSON字段是否直接拼SQL一律参数化JSON字段是否拼Shell命令参数数组执行 白名单JSON字段是否拼XML禁止外部实体 优先结构化序列化JSON解析后是否递归合并过滤危险键 白名单字段接口是否需要接收未知字段additionalProperties:false5. 一次内部安全测试的排错复盘从无告警到定位注入点5.1 现象扫描全绿数据却不对劲有次我对一个内部测试系统做例行检查接口接收的是JSON body自动化扫描报告干干净净。但我手工翻日志时发现一个有意思的现象某个接口接收num: 1时返回一条记录接收num: 1 数字后面带个空格时日志里出现了一段SQL执行错误的时间记录。这个差异很关键。如果字段被参数化传字符串1 和数字1数据库要么报类型不匹配要么把字符串当成值去比较但不太会出现“SQL语句本身执行异常”之类的时间标记。出现异常说明这个字段很可能被拼接到SQL里了。5.2 排查链路三层定位法我没有一上来就写payload而是按下面三层顺序做的定位。第一层看网关和框架日志。先确认请求的原始body有没有被网关改动比如是不是被统一urlencode了是不是经过了某种参数解析。很多框架会解析JSON body也可能在中间件里做了转义这些都会改变后续判断。第二层在接口层打印参数。我习惯在怀疑的接口入口处临时打一行日志输出JSON解析后的完整对象确认num字段在后端到底是什么类型。这一步能排除“前端传了字符串而后端按数字处理”这类普通bug。第三层做最小复现。构造不同输入观察响应差异——正常数字、超长数字、负数、带空格的字符串。如果某个输入导致响应码从200变成500或者返回数据集明显异常就把触发条件和输入样本记录到测试报告里。这套流程的核心是“像调试代码一样去定位漏洞”。你不需要知道很多花哨的payload只需要对输入输出之间的异常差异足够敏感。5.3 修复与回归验证定位到问题字段后修复方案分两步落地。第一步在入口加Schema校验。这个字段在白名单里只允许整数类型且范围是1到10000超范围直接拒绝。第二步把后端SQL改成参数化查询从数据访问层根治拼接。回归验证时我会覆盖三类用例正常业务用例确认功能没有回退边界值用例确认1、最大值、最小值都正常恶意特征值用例确认之前会让接口异常的那些输入现在只会被校验逻辑拒绝不会到达数据库。修复后最重要的一件事是复测同一套异常样本确认全链路不再出现SQL执行异常的日志。这一步做到了才算是真正闭环。6. 为什么搜“json注入”会搜出书源、Minecraft和依赖注入6.1 同一个词四个世界的四种含义搜索引擎的关键词不会区分上下文所以你会看到“json注入”这个词同时关联着几个完全不同的圈子。安全领域里的“json注入”也就是这篇博文讨论的以JSON为载体的注入攻击。小说阅读器圈子里说的“注入书源”是把JSON格式的书源内容导入阅读器App很多用户把它简化称为“json注入”所以热搜词里会出现“2026书源json”之类的内容。Java开发圈子里的“依赖注入”“构造函数注入”“属性注入”讲的是Spring容器控制反转的设计思想跟安全无关但因为都带“注入”也会混进来。Minecraft启动器报错、Edge浏览器加载本地JSON、LabelMe标注文件转TXT则纯粹是JSON文件读写问题和安全没有任何关系。所以如果你是为了排查某个实际问题才搜到这个标题先在脑子里确认一下你要找的是安全漏洞相关的内容还是工具使用相关的内容。两者差别很大。6.2 安全搜索名词速查针对想进入安全方向的朋友我整理了一份和“json注入”相关的高频搜索词方便快速定位学习资源搜索词对应的学习内容pikachu数字型注入Pikachu靶场的SQL注入关卡dvwa sql注入DVWA靶场适合零基础入门ctfhub布尔注入CTFHub平台的SQL布尔盲注入题目xxe注入针对XML外部实体的注入问题命令注入攻击详解Shell命令拼接导致的注入问题dnslog注入靶场用DNSLog外带数据的盲注练习环境sql注入replace()过滤replace函数时的绕过思路需要强调的是这些关键词对应的内容只适合在本地靶场、CTF平台、以及你有明确授权的测试环境里使用。在未授权系统上做任何安全性验证都是违规行为这一点没有讨论余地。6.3 建议的练习路径如果你是零基础建议按这个顺序推进。先搭一个DVWA靶场把SQL注入的低中高三个难度完整走一遍理解“拼接位置不同带来的差异”。然后到Pikachu靶场把数字型、字符型、搜索型等不同注入形态过一遍建立“不同字段位置不同注入思路”的直觉。接着在CTFHub上做XXE和命令注入的专项题目把思路从SQL扩展到其他解释器。最后回到本篇文章的防御链路自己试着写一个接收JSON的接口再用Schema校验和参数化查询把这条路堵死。练完之后你会发现真正重要的不是记住某个payload而是建立一种“输入到解释器之前必须经过信任边界”的工程习惯。说说我个人的体会吧。安全这行做久了最深的感受是看起来正常的数据往往最危险。JSON格式是干净、规范、无辜的问题几乎都出在解析之后那道“理所当然”的拼接上。我后来养成的习惯是任何外部输入要进入解释器先强制走参数化或白名单把这条路堵死再谈业务逻辑。开发的时候多花十分钟后面就能少熬无数个排查漏洞的夜。希望这篇关于json注入的梳理能帮你少踩一个坑。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进