typeorm 0.3 的 find、findone 等方法默认参数化查询,天然防sql注入;原生 sql 必须用 query + 参数数组,动态结构部分(如表名、排序字段)须白名单校验。

用 find、findOne 等模型方法时,默认就是安全的
TypeORM 0.3 的 find、findOne、count、remove 等实体方法,底层自动走参数化查询。只要你不手动拼接 SQL 字符串,这些调用天然防注入。
比如:
const users = await userRepository.find({ where: { email: req.query.email } });
等价于预编译语句 SELECT * FROM users WHERE email = $1(PostgreSQL)或 WHERE email = ?(MySQL),参数由驱动安全绑定。
- 所有
where对象里的值(字符串、数字、布尔、Date)都会被当作参数处理 -
ILIKE、Between、In等操作符也支持,且仍保持参数化 - 嵌套对象如
{ profile: { isActive: true } }同样安全,TypeORM 会递归解析为参数 - ⚠️ 注意:如果字段名来自用户输入(如
req.query.field),不能直接塞进where键名,必须白名单校验
手写原生 SQL 必须用 query + 参数数组,禁用模板字符串插值
TypeORM 提供 query 方法执行原始 SQL,但极易踩坑。唯一安全写法是:SQL 字符串里只含占位符($1、$2 或 ?),参数单独传数组。
正确示例(PostgreSQL):
const result = await dataSource.query( 'SELECT * FROM users WHERE status = $1 AND created_at > $2', [req.body.status, req.body.since]);
错误示例(全部高危):
-
dataSource.query(`SELECT * FROM users WHERE id = ${req.body.id}`)—— 模板字符串插值 = 拼接 -
dataSource.query('SELECT * FROM users WHERE name = \'' + req.body.name + '\'')—— 手动拼接 -
dataSource.query('SELECT * FROM ' + tableName + ' WHERE ...')—— 表名不可参数化,必须白名单
动态排序、分组、表名必须白名单硬编码,不能“过滤后拼接”
SQL 结构部分(ORDER BY 字段、GROUP BY 列、FROM 表名、JOIN 别名)无法用占位符,TypeORM 不支持参数化它们。任何尝试“转义后拼接”的做法都无效。
安全做法是显式声明允许值:
const allowedSortFields = { name: 'name', email: 'email', createdAt: 'created_at' };<br>const sortField = allowedSortFields[req.query.sort] || 'createdAt';<br>const direction = ['ASC', 'DESC'].includes(req.query.dir) ? req.query.dir : 'ASC';<br><br>const sql = `SELECT * FROM users ORDER BY ${sortField} ${direction}`;<br>await dataSource.query(sql);
- 不要用正则删掉单引号或分号——攻击者可用
/**/、反引号、Unicode 注释绕过 - 不要信任
req.query.table经过escape()就安全;escape()只适配 MySQL,且不作用于结构上下文 - 如果业务真需高度动态,考虑用视图或存储过程封装逻辑,把动态部分收口到 DB 层
别把 queryBuilder 当“免检通道”,addSelect 和 where 的写法决定是否安全
TypeORM 的 createQueryBuilder 很灵活,但安全性取决于你怎么用。它的 where 方法接受对象或字符串,只有前者安全;addSelect、groupBy 等方法若传入用户输入的字符串,立刻失守。
安全写法:
const posts = await postRepository.createQueryBuilder('post')<br> .innerJoinAndSelect('post.author', 'author')<br> .where('post.status = :status', { status: req.query.status }) // ✅ 命名参数<br> .andWhere('author.id IN (:...ids)', { ids: req.body.authorIds }) // ✅ 展开数组参数<br> .getMany();
危险写法:
-
.where('post.title LIKE \'%' + req.query.q + '%\'')—— 字符串拼接 -
.addSelect('COUNT(*) as cnt').groupBy('post.' + req.query.groupBy)—— 动态字段未白名单 -
.having('cnt > ' + req.query.minCount)—— 数字也要校验范围,不能只信类型
真正卡住 SQL 注入的,永远是那个 :status 或 $1 占位符背后由数据库驱动完成的预编译绑定。其他所有“加固”动作,都是围绕它打辅助。动态结构部分没有银弹,只有白名单和最小权限账户兜底。











