很多人以为SQL注入只发生在URL参数或普通表单里,只要把请求体改成JSON,攻击者就无从下手。这种想法在大量API和前后端分离架构普及的今天,已经成为最危险的认知盲区。JSON请求体中的SQL注入非但没有消失,反而因为解析方式、数据类型强制转换、NoSQL与SQL混用等原因,演化出了更隐蔽的变异形式。攻击者利用JSON的结构化特性,可以轻松绕过只针对表单编码设计的WAF规则,直接在JSON的字符串值、数字、数组甚至嵌套对象中植入恶意载荷。
JSON字符串值中的经典注入变异最直接的形式仍然出现在JSON的字符串值里。后端接收到JSON后,很多开发习惯直接取出字段值拼接到SQL语句中。例如一个登录接口接收:
{
"username": "admin",
"password": "123456"
}
后端代码可能这样处理:
String query = "SELECT * FROM users WHERE username = '" + json.getString("username") + "' AND password = '" + json.getString("password") + "'";
攻击者将payload放入password字段:
{
"username": "admin",
"password": "' OR '1'='1"
}
这在原理上与表单注入无异,但WAF如果只对application/x-www-form-urlencoded格式做检测,而对application/json放行,攻击就能轻易得手。更隐蔽的做法是利用JSON的转义特性。JSON中双引号需要反斜杠转义,但后端解析库会自动去掉反斜杠还原原始字符串。攻击者可以构造:
{
"username": "admin\" OR 1=1 --",
"password": "anything"
}
当JSON解析器处理这个字符串时,\"会被还原为",最终到达SQL拼接环节的就是包含注入代码的完整字符串。这种利用JSON转义机制绕过前端校验和后端简单过滤的手法,在实际渗透测试中成功率极高。
JSON数字与布尔值导致的隐式注入这是JSON注入中最容易被忽视的变异形式。JSON有明确的数字类型和布尔类型,与字符串严格区分。很多开发在处理JSON时,会直接使用getInt()、getBoolean()等方法获取值,然后拼入SQL。表面上看,数字和布尔值不可能包含字符串注入payload,但问题出在类型转换的边界上。当后端使用弱类型语言或框架自动将JSON数字转换为字符串拼入SQL时,攻击者可以通过精心构造的数字边界值触发异常行为。更危险的情况是,某些ORM框架在处理JSON字段映射时,会将数字直接拼接而不做参数化。例如一个分页查询接口:
{
"page": 1,
"size": 10
}
如果后端直接将size拼入LIMIT子句:
String query = "SELECT * FROM products LIMIT " + json.getInt("size");
攻击者虽然不能直接修改数字类型注入字符串,但可以尝试发送超大数据或负数探测SQL执行边界。真正致命的变异发生在某些框架允许数字以字符串形式传递但后端仍将其拼接入SQL的场景。攻击者发送:
{
"page": "1",
"size": "10; DROP TABLE products; --"
}
如果后端没有严格校验数据类型,直接将字符串值取出拼接,就会造成注入。这种类型混淆的漏洞在Node.js、PHP等弱类型语言中尤为常见。布尔值同样存在利用空间,某些后端会将true/false转换为1/0拼入SQL,如果转换逻辑存在缺陷,攻击者可以尝试注入null值或特殊Unicode字符来破坏SQL结构。
JSON数组和嵌套对象的二阶注入JSON的数组和嵌套对象结构为攻击者提供了更丰富的注入载体。很多业务接口允许传入数组作为IN子句的参数,例如批量查询:
{
"ids": [1, 2, 3]
}
后端常见的不安全处理方式是将数组元素直接拼接成逗号分隔的列表:
String ids = String.join(",", json.getJSONArray("ids"));
String query = "SELECT * FROM orders WHERE id IN (" + ids + ")";
攻击者可以构造数组元素包含注入代码:
{
"ids": ["1) UNION SELECT username, password FROM users --", 2, 3]
}
拼接后SQL变为:
SELECT * FROM orders WHERE id IN (1) UNION SELECT username, password FROM users --, 2, 3)
这种注入极其隐蔽,因为WAF通常不会对JSON数组的每个元素做SQL注入检测,尤其是当数组元素类型不一致时。嵌套对象则提供了更复杂的攻击面。假设一个用户资料更新接口接收:
{
"user": {
"name": "John",
"address": {
"city": "New York",
"zip": "10001"
}
}
}
后端可能递归遍历所有字段并更新数据库。攻击者可以在深层嵌套的字段中植入payload:
{
"user": {
"name": "John",
"address": {
"city": "New York' OR '1'='1",
"zip": "10001"
}
}
}
如果更新语句使用拼接方式构建SET子句,city字段的值会直接注入SQL。这种深层嵌套的注入点极难被自动化扫描工具发现,因为工具往往只检测顶层字段,而忽略嵌套层级较深的值。
JSON Key本身的注入风险绝大多数开发和安全人员只关注JSON的value部分,完全忽略了key也可能被拼入SQL。某些动态查询构建器会根据JSON的key生成SQL列名或条件。例如一个灵活的查询接口:
{
"filters": {
"username": "admin",
"status": "active"
}
}
后端遍历filters对象的key来动态构建WHERE子句:
for (String key : filters.keySet()) {
query += " AND " + key + " = '" + filters.get(key) + "'";
}
攻击者可以构造恶意的key:
{
"filters": {
"username": "admin",
"1' OR '1'='1": "anything"
}
}
生成的SQL变成:
SELECT * FROM users WHERE 1=1 AND username = 'admin' AND 1' OR '1'='1 = 'anything'
虽然语法可能报错,但攻击者可以通过调整payload使SQL语法正确闭合。这种攻击方式完全绕过了所有只检测value的防护手段。更危险的是,某些ORM允许通过JSON key指定排序字段或分组字段,如果key直接拼入ORDER BY或GROUP BY子句,注入成功率将大幅提升。
利用JSON注释和特殊字符绕过WAFJSON标准本身不支持注释,但很多JSON解析器在实现时为了兼容性允许C风格注释或shell风格注释。攻击者可以利用这一特性在JSON中插入注释来混淆WAF检测。例如:
{
"username": "admin'/*",
"password": "*/ OR 1=1 --"
}
当后端将两个字段拼接在一起时,中间的注释符号会注释掉原本的SQL片段。更常见的手法是利用JSON中的Unicode转义。JSON允许使用\uXXXX格式表示Unicode字符。攻击者可以将SQL关键字进行Unicode编码来绕过基于正则的WAF规则:
{
"username": "admin",
"password": "' \u004F\u0052 1=1 --"
}
\u004F和\u0052分别是字母O和R的Unicode码点。JSON解析器会将其还原为OR,但WAF在原始请求层面看到的只是\u004F\u0052字符串,无法识别为SQL关键字。这种编码绕过手法可以扩展到整个payload,攻击者可以将SELECT、UNION、DROP等关键字全部编码,WAF的静态规则几乎无法覆盖所有Unicode变体。此外,JSON中的反斜杠转义序列也可以用来构造多义性payload。例如:
{
"search": "test\\' OR 1=1 --"
}
这里双反斜杠在JSON解析后变为单个反斜杠,如果后端又对这个反斜杠做了某种处理,可能导致引号逃逸。这种多层转义的叠加效应让注入检测变得异常复杂。
Content-Type混淆与解析器差异利用现代Web框架通常根据Content-Type请求头来决定如何解析请求体。攻击者可以通过篡改Content-Type来触发不同的解析路径。例如,服务器可能同时支持application/json和application/x-www-form-urlencoded两种格式。攻击者发送一个Content-Type为application/json的请求,但请求体却是表单格式,或者反过来。某些框架在这种情况下会尝试用JSON解析器去解析表单数据,或者将JSON数据当作字符串处理,这个解析过程中的异常处理逻辑可能暴露出注入点。更高级的攻击手法是利用字符集差异。JSON规范要求使用UTF-8编码,但攻击者可以尝试发送UTF-16或UTF-7编码的JSON,某些服务器在转码过程中可能产生字符混淆,使得WAF看到的是乱码,而后端应用看到的却是可执行的SQL payload。这种攻击在老旧系统或配置不当的中间件上仍然有效。
NoSQL注入与SQL注入的JSON融合在现代架构中,很多应用同时使用关系型数据库和NoSQL数据库,中间通过API网关或微服务进行数据聚合。攻击者发现,针对MongoDB等NoSQL数据库的注入操作符,可以通过JSON请求体传递,而如果这些数据后续被用来构建SQL查询,就会产生跨数据库的注入链。例如,MongoDB的$gt、$ne等操作符在JSON中表现为:
{
"age": {"$gt": 18}
}
如果后端将这个JSON对象直接用于构建SQL的WHERE条件,且没有正确处理$gt这样的特殊key,就可能产生SQL注入。更隐蔽的情况是,某些数据中台会将JSON查询条件转换为SQL,转换过程中的语法树解析如果存在漏洞,攻击者可以构造同时符合NoSQL和SQL语法的混合payload,实现一次攻击穿透两层数据库。这种攻击手法在数据湖、数据仓库等需要跨源查询的场景中尤为危险。
参数化查询在JSON场景下的正确实践解决JSON请求体中的SQL注入,核心原则仍然是参数化查询,但具体实施需要针对JSON的处理特点做调整。首先,必须使用预编译语句和参数绑定,无论值来自JSON的字符串、数字还是布尔类型。对于IN子句的动态数组,应该使用数据库驱动提供的数组绑定功能,而不是手动拼接。以Java的JDBC为例:
String query = "SELECT * FROM orders WHERE id IN (?)";
// 使用Array类型绑定
Array array = connection.createArrayOf("INTEGER", ids);
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setArray(1, array);
对于动态列名或表名这种无法参数化的场景,必须使用白名单校验。将JSON中传入的字段名与预定义的白名单进行严格比对,任何不在白名单中的输入直接拒绝。白名单应该硬编码在代码中,而不是从配置文件或数据库读取。对于嵌套JSON对象,需要递归遍历所有层级,对每个叶子节点的值做参数化处理或白名单校验。建议在数据访问层之上增加一个专门的JSON清洗层,负责将所有外部输入标准化后再传递给ORM或SQL执行器。这个清洗层应该具备以下能力:严格验证JSON schema,拒绝未知字段;对所有字符串值进行SQL注入特征检测,但注意不能仅依赖正则黑名单;对数字和布尔值进行类型强制约束,不允许类型混淆;对数组元素进行逐个校验,不允许数组元素类型不一致。
JSON请求体中的SQL注入之所以危险,不是因为技术原理有多复杂,而是因为它利用了开发者对数据格式的盲目信任和防护工具的覆盖盲区。攻击者不断变异注入形式,从简单的字符串拼接发展到利用类型系统、解析器差异、Unicode编码等多维度绕过。防御的关键在于将安全控制前置到JSON解析完成后的第一时间,对所有提取出的数据保持同等的警惕,无论它来自URL参数、表单字段还是JSON的深层嵌套结构。参数化查询和白名单校验这两项基础实践,在JSON场景下依然是不可替代的核心防线。
