TypeORM中SQL注入防护需同时满足参数化与白名单校验:where条件须用对象传参(如:date)而非字符串拼接;JSONB路径等动态字段名必须严格白名单校验;QueryBuilder动态参数需生成唯一键名;原生query()必须用数组传参,结构部分仍需白名单。

Raw SQL 的 where 参数化必须用对象传参,不能拼字符串
TypeORM 的 Raw 函数支持参数化,但只在你显式传入第二个参数(参数对象)时生效。如果只写 Raw((alias) => `${alias} > '${req.query.date}'`),就等于裸拼 SQL,'${req.query.date}' 会被原样插入,攻击者输 2020-01-01' OR 1=1 -- 就能绕过条件。
正确做法是:
- SQL 字符串里只放占位符,比如
:date或$1 - 第二个参数必须是对象,且键名要和占位符一致:
{ date: req.query.date } - 值本身仍需校验:日期字段应做格式解析(如
new Date(req.query.date)),非法值直接拒绝
动态字段名(如 JSONB 路径)必须白名单校验
像 row_data->>'${key}' 这种写法,${key} 是字段路径,无法参数化。TypeORM 不拦截、数据库不转义,攻击者传 Location'; DROP TABLE users; -- 就会执行。
必须提前定义允许的路径列表,并严格比对:
const ALLOWED_JSON_PATHS = ['Location', 'Status', 'CreatedAt'];if (!ALLOWED_JSON_PATHS.includes(req.query.path)) throw new Error('Invalid path');- 之后才能安全拼进 SQL:
row_data->>'${req.query.path}'
QueryBuilder 的 andWhere() 用命名参数时,键名必须动态计算
当你用 query.andWhere(`data->>'${key}' ILIKE :searchVal`, { searchVal: `%${value}%` }),看似参数化了,但问题在 searchVal 是固定字符串——如果多个条件共用同一个键名,后一个会覆盖前一个,导致查询错乱或报错。
正确方式是生成唯一参数名,并用方括号语法确保动态绑定:
- 构造参数名:
const paramName = `val_${Date.now()}_${Math.random().toString(36).substr(2, 5)}` - 使用计算属性:
{ [paramName]: `%${value}%` } - SQL 中对应写:
:${paramName}
原生 query() 调用必须用数组参数,禁用模板字符串
queryRunner.query() 是 TypeORM 最底层的原始执行接口,它完全不处理字符串插值。写成 queryRunner.query(`SELECT * FROM users WHERE name = '${req.query.name}'`) 等同于手写 SQL 注入漏洞。
唯一安全写法是:
- SQL 字符串中只含占位符:
$1(PostgreSQL)或?(MySQL) - 参数单独传数组:
queryRunner.query(sql, [req.query.name]) - 字段名、表名、ORDER BY 等结构部分仍需白名单校验,不能靠这个接口兜底
最容易被忽略的是:白名单校验和参数化必须同时存在。漏掉字段名校验,参数再安全也挡不住注入;参数名写错或重复,查询可能失败,还可能把数据库错误细节暴露给前端。

















