JSON字段里的SQL注入,本质上是一种上下文切换攻击。开发者在解析HTTP请求时,注意力往往集中在URL参数或普通表单字段上,当数据以JSON格式嵌套在请求体中,特别是多层嵌套的JSON对象或数组时,后端代码在处理这些反序列化后的数据时,容易放松警惕。攻击者正是看准了这一点,他们不再把恶意载荷放在明面上,而是将其藏匿在看似无害的JSON属性值中,绕过那些只对Query String或标准POST表单进行检测的防御机制。
攻击的触发场景与根本原因这种攻击最常见于API端点。现代Web应用大量使用AJAX或Fetch API进行前后端数据交互,请求头Content-Type通常被设置为application/json。后端框架如Spring Boot、Express.js或Django REST Framework会自动将JSON字符串解析为原生字典或对象。问题在于,当业务逻辑层直接获取这些解析后的值,并用于拼接SQL语句时,开发者潜意识里认为这些数据已经过了一层“结构化处理”,是安全的。实际上,JSON解析器只负责将文本转换为数据结构,它完全不会对值中的SQL关键字进行任何过滤。一个典型的攻击载荷看起来可能是这样的:
{
"username": "admin",
"preferences": {
"theme": "dark",
"language": "zh' OR '1'='1"
}
}
在这个例子中,language字段的值被直接拼接到类似SELECT * FROM settings WHERE lang = 'zh' OR '1'='1'的查询中,导致条件恒真。更隐蔽的情况是,攻击载荷可能藏在JSON数组里,或者利用JSONB/JSON类型的数据库字段进行二次注入。例如,在PostgreSQL或MySQL中,如果应用逻辑是将整个JSON对象存储到数据库的JSON类型列中,随后又使用JSON_EXTRACT或->>操作符取出内部字段进行动态查询拼接,注入就会发生在数据库内部层面,而不是应用层。这种二次注入极难被静态代码分析工具发现。
绕过WAF的经典手法传统的Web应用防火墙通常对Query String和表单体进行深度检测,但对JSON Body的检测策略往往比较宽松,尤其是当JSON结构非常复杂时。攻击者利用JSON的语法灵活性来混淆载荷,例如使用Unicode转义符。SQL注入中的单引号可以被表示为\u0027,空格可以被表示为\u0020。如果后端在JSON解析后没有进行规范化处理,这些转义后的字符会直接被还原为原始字符,从而绕过基于正则表达式的签名检测。另一种高级手法是利用JSON中的注释或重复键行为。虽然JSON标准不支持注释,但某些解析器在宽松模式下允许,或者攻击者利用不同中间件对重复键处理方式的差异,将恶意载荷藏在被覆盖的键中,测试防御系统的解析一致性。
数据库JSON函数带来的二次注入风险现代关系型数据库如MySQL 5.7+、PostgreSQL 9.4+都提供了强大的原生JSON支持。这带来了一个极具迷惑性的攻击面。假设应用代码接收前端JSON,直接存入一个JSON类型的列中,然后在某个查询中使用JSON_EXTRACT取出值,并用于另一个查询的动态条件拼接。这个过程中的SQL注入点不在写入时,而在读取并二次使用时。例如,攻击者提交的数据被存储为:
{"profile": {"city": "Beijing' UNION SELECT ... -- "}}
当后端执行SELECT JSON_EXTRACT(data, '$.profile.city') FROM users WHERE id = 1获取城市名,并立即拼接到SELECT * FROM shops WHERE city = 'Beijing' UNION SELECT ... -- '时,注入就发生了。这种情况下,WAF在数据入库时看到的是一个完整的JSON对象,很难识别出其中的字段值将来会构成SQL注入威胁。只有理解了数据全生命周期的安全架构师,才会在这个环节设置防御。
防御策略:从输入验证到查询参数化最根本的防御措施仍然是使用预编译语句和参数化查询。无论数据来自URL参数、表单字段还是JSON嵌套结构,最终在拼装SQL时都必须使用占位符。在Java的JDBC PreparedStatement、Go的database/sql、Python的sqlite3或psycopg2中,都应该严格遵循这一原则。对于JSON数据,在反序列化后,将每个需要用于数据库查询的值绑定到参数上,而不是进行字符串拼接。代码示例如下:
# 错误做法:直接拼接
city = request_json['profile']['city']
cursor.execute(f"SELECT * FROM shops WHERE city = '{city}'")
# 正确做法:参数化
city = request_json['profile']['city']
cursor.execute("SELECT * FROM shops WHERE city = ?", (city,))
如果业务场景确实需要动态表名、列名或ORDER BY/GROUP BY字段,无法直接使用参数化,那么必须执行严格的白名单验证。从JSON中提取出的这些标识符,必须在一个预定义的合法集合中进行匹配,不匹配则拒绝执行。绝对不能仅仅依赖转义或黑名单过滤,因为JSON编码和数据库字符集的差异可能导致转义失效。
ORM框架的陷阱与正确使用姿势很多开发者误以为使用了Hibernate、SQLAlchemy或GORM这类对象关系映射框架就天然免疫SQL注入。这是一个危险的错觉。ORM能防御大部分基于值的注入,但当使用原生SQL或动态查询构建功能时,风险依然存在。特别是在JPA中使用@Query注解并拼接JPQL,或在SQLAlchemy中使用text()函数直接执行字符串时,如果传入了从JSON解析出的未处理变量,注入就会发生。正确的做法是利用ORM提供的参数绑定机制,例如在Hibernate中使用:paramName占位符,在SQLAlchemy中使用:param并传递字典参数。对于JSON字段,ORM框架通常提供了类型映射,将JSON列映射为实体中的字符串或自定义对象,在映射过程中不会自动进行防注入处理,安全责任依然在开发者编写的查询逻辑上。
多层防御体系的构建单一防御手段在面对JSON SQL注入时容易失效,因此需要构建纵深防御体系。第一层是输入验证,在JSON反序列化后,对每个字段进行类型、长度、格式和范围的严格校验。使用JSON Schema验证库可以自动化这一过程,明确定义每个属性的数据类型和约束,拒绝任何不符合Schema的请求。第二层是参数化查询,确保所有数据访问都通过安全接口。第三层是数据库权限最小化,应用连接的数据库账户只应拥有执行必要操作的权限,禁止DROP、ALTER等高危操作,即使注入成功也能限制损害范围。第四层是运行时检测,部署能够理解JSON结构和SQL语义的RASP(运行时应用自我保护)工具,在应用内部监控实际的查询执行,当检测到SQL语句结构被外部输入异常改变时立即阻断。
针对JSONB列的特殊加固在使用PostgreSQL的JSONB或MySQL的JSON列时,如果应用需要根据JSON内部字段的值进行查询,应优先使用数据库提供的参数化JSON操作符,而不是拼接字符串。例如,PostgreSQL中可以使用$1::jsonb这样的占位符传递整个JSON值,或者使用jsonb_path_exists等函数配合参数化路径表达式。MySQL中可以使用JSON_CONTAINS、JSON_EXTRACT与占位符结合。避免在SQL字符串中直接嵌入用户提供的JSON路径表达式或值。如果必须动态构建JSON路径,同样需要白名单验证路径中的键名部分,因为JSON路径本身也可能成为注入载体,攻击者可以构造特殊的路径表达式来绕过预期的数据访问范围。
测试与审计方法常规的自动化扫描器对JSON SQL注入的检测率较低,因为它们通常只对标准注入点发送测试载荷。安全测试人员需要针对API端点专门构造测试用例,将SQL注入载荷放入JSON的各个层级:根级别字段、嵌套对象字段、数组元素、以及JSON字段名本身。测试时需要注意Content-Type头必须正确设置为application/json,并且观察响应时间、错误信息或布尔状态差异。代码审计方面,应重点搜索所有从JSON对象获取值并用于数据库查询的代码路径,特别是那些使用了字符串格式化、拼接操作的地方。对于使用动态查询构建器的情况,需要追踪JSON数据的流向,确认最终执行时是否使用了参数化。日志记录也是关键,在生产环境中记录所有反序列化后的JSON查询参数值,并建立异常检测规则,当发现这些值包含SQL关键字模式时触发告警。
JSON作为数据交换格式的普及,使得攻击面从平面的表单扩展到了立体的嵌套结构。攻击者利用JSON字段传递SQL注入载荷,本质上是利用了开发者对结构化数据信任度的差异。防御的关键不在于区分数据来源,而在于统一且严格地执行参数化查询原则,并配合Schema验证、权限控制和运行时监控,形成覆盖数据全生命周期的防护网。任何从外部输入到达数据库执行器的路径上,都不应该存在字符串拼接的环节,这个原则无论数据是藏在URL参数里,还是藏在十层嵌套的JSON对象里,都同样适用。
