typeorm的where条件拼接可能引发sql注入,因字段名动态拼接(如[req.query.field])、字符串模板(如status = '${req.query.status}')及未校验的in参数均绕过参数化;安全做法是字段白名单、值类型强转、querybuilder参数化。

TypeORM 的 where 条件拼接为什么可能出问题
TypeORM 的 where 选项本身是安全的——比如 userRepository.find({ where: { username: req.body.username } }) 会自动参数化。但一旦你用对象展开、条件合并或字符串拼接构造 where 对象,风险就来了。典型错误是把用户输入直接塞进 key 名或嵌套表达式里,例如:{ [userInput]: value } 或 `id = ${req.query.id}`。TypeORM 不会对 key 做任何校验,它原样当字段名处理,而字段名无法参数化。
哪些写法会触发 SQL 注入(且 TypeORM 不拦截)
以下写法看似“用了 ORM”,实则等同于裸拼 SQL:
-
userRepository.find({ where: { [req.query.field]: req.query.value } })—— 字段名动态,req.query.field可传password'; DROP TABLE users; -- -
userRepository.find({ where: `status = '${req.query.status}'` })—— 直接字符串模板,完全绕过参数化 -
userRepository.createQueryBuilder().where(`name LIKE '%${req.query.q}%'`)——where()接字符串时,TypeORM 不做转义 -
userRepository.find({ where: { id: In(req.query.ids.split(',')) } })—— 若ids是逗号分隔字符串,split后未校验每个元素是否为数字,In会拼成IN (1,2, 'x' OR '1'='1')
安全写法:白名单 + 显式类型转换 + QueryBuilder 参数化
真正可控的方式不是“避免拼接”,而是“控制拼接内容”:
- 字段名必须来自硬编码白名单:
const ALLOWED_FIELDS = ['username', 'email', 'status']; const field = ALLOWED_FIELDS.includes(req.query.field) ? req.query.field : 'username'; - 值必须做类型强约束:
const id = parseInt(req.query.id, 10); if (isNaN(id)) throw new Error('Invalid ID'); - 模糊搜索用
Like+ 参数化:where: { name: Like(`%${req.query.q}%`) }(注意:Like内部已参数化,但q仍需长度和字符限制) - 复杂条件优先用 QueryBuilder 并只对值参数化:
.where('status = :status', { status: req.query.status })—— 这里的:status是安全的,但字段名status仍需白名单校验
多条件组合时最容易漏掉的校验点
业务常需要支持 ?sort=createdAt&order=DESC&filter=status:active 这类查询,此时三个地方都得守门:
-
sort和order都要白名单:['ASC', 'DESC']和['id', 'createdAt', 'updatedAt'] -
filter解析后,每个键(如status)必须在允许字段列表中;每个值(如active)不能是任意字符串,应映射到预定义枚举或正则校验(如/^[a-z]+$/) - 如果支持
filter=price:100-200,区间解析后必须确保min ,且两个值都是数字,不能是 <code>100; DROP TABLE...
最麻烦的不是写校验逻辑,而是每次新增一个可筛选字段,都要同步更新白名单和类型规则——漏一条,就等于在 DAO 层开了个口子,TypeORM 自己不会提醒你。











