JSON字段查询易被忽略防注入,因Op.contains等操作混入用户输入会绕过参数化,且PostgreSQL jsonb_path_exists()等函数不支持参数化路径,必须用命名绑定加白名单校验。

JSON字段查询为什么容易被忽略防注入
Sequelize对JSON字段(如PostgreSQL的JSONB、MySQL 5.7+的JSON类型)的支持,常让开发者误以为“ORM自动处理=天然安全”。但实际中,Op.contains、Op.overlap、fn('jsonb_path_exists')等操作,一旦混入用户输入且未加约束,就会绕过参数化机制。尤其当用literal()包装路径表达式或用sequelize.query()执行原生JSON函数时,风险陡增。
PostgreSQL jsonb_path_exists()这类函数必须用命名绑定
PostgreSQL提供jsonb_path_exists()做复杂JSON匹配,但它不接受参数化占位符——SQL协议本身不允许把JSON路径表达式当参数传。所以你不能写WHERE jsonb_path_exists(data, ?)。错误做法是拼接字符串:literal(`jsonb_path_exists(data, '$."${req.query.path}"')`),此时req.query.path直接进SQL。
- ✅ 正确方式:用命名绑定 + 白名单校验路径结构
sequelize.query("SELECT * FROM items WHERE jsonb_path_exists(data, :path)", { replacements: { path: `$.${validateJsonPath(req.query.key)}` } })- 白名单校验函数示例:
function validateJsonPath(key) { return ['name', 'tags', 'metadata.version'].includes(key) ? key : 'name'; } - ❌ 禁止用
req.query.path直接插进literal()或模板字符串
MySQL JSON_CONTAINS()的value参数必须走replacements
MySQL的JSON_CONTAINS()接受三个参数:JSON_CONTAINS(doc, path, value)。其中doc和path是SQL结构部分,不可参数化;但value是数据,必须参数化。常见破防是把整个表达式塞进literal():
- ❌ 危险:
where: { [Op.and]: [literal(`JSON_CONTAINS(data, "$.tags", "${req.body.tag}")`)] } - ✅ 安全:
where: { [Op.and]: [literal('JSON_CONTAINS(data, "$.tags", :tag)')] }, replacements: { tag: req.body.tag } - 注意:
replacements只在sequelize.query()中生效;Model.findAll()不支持replacements,此时应改用Op.contains对象语法 - 对于简单键值匹配,优先用
Op.contains:where: { data: { [Op.contains]: { tags: req.body.tag } } }——它由Sequelize内部转为安全参数化语句
动态JSON路径字段名必须硬编码白名单
JSON字段里查data->>'$.user.name'还是data->>'$.config.timeout',取决于业务逻辑。但字段名(user、config)属于SQL结构,无法参数化。攻击者若控制该字段名,可构造user"; DROP TABLE users; --类payload。
- 白名单必须硬编码,不能靠正则过滤单引号或分号
- 例如允许的路径前缀:
const allowedJsonRoots = ['user', 'config', 'settings', 'profile'] - 取值前强制校验:
const root = allowedJsonRoots.includes(req.query.root) ? req.query.root : 'user' - 再组合查询:
attributes: [[literal(`data->>'$.${root}.name'`), 'display_name']] —— 这里<code>root已受控,name仍需额外白名单或固定值
JSON查询的安全边界比普通字段更模糊——它既像数据又像结构。最容易被忽略的是:你以为在操作“值”,其实正在拼接“语法”。所有来自请求的路径片段、键名、操作符,只要进literal()或原生SQL字符串,就必须经过白名单或静态映射,没有例外。


















