TypeORM 本身不产生 SQL 注入,风险仅来自手动字符串拼接;应优先使用 where 对象或参数化 QueryBuilder,Raw 必须传参数对象而非字符串插值,所有用户输入须经参数化处理。

直接用 where 对象或 QueryBuilder 的参数化机制,别拼字符串。TypeORM 本身不产生 SQL 注入漏洞,问题只出在你绕过它的安全层、手动拼接 SQL 的那一刻。
为什么 raw() 不等于裸写 SQL
Raw() 看似危险,但只要第二个参数传了参数对象,它就仍是参数化查询。TypeORM 会把占位符(如 :date)交给数据库驱动做预编译处理,不是字符串替换。
- ✅ 安全写法:
currentDate: Raw((alias) => `${alias} > :date`, { date }) - ❌ 危险写法:
currentDate: Raw((alias) => `${alias} > '${date}'`)—— 这里单引号包裹 + 字符串插值,date值被直接拼进 SQL,完全暴露给注入 - ⚠️ 注意:
Raw的第一个参数是函数,不是模板字符串;别图省事写成Raw(`${alias} > :date`, { date }),这会报错且失去类型提示
多条件动态组合时,优先用 where 对象而非手写 SQL
绝大多数“多条件”场景(比如搜索页的 N 个可选字段),find() 的 where 参数已足够灵活,不需要动 Raw 或 QueryBuilder。
- 字段为空时不参与查询:把条件对象按需组装,只加入有值的 key,例如
const where = {}; if (name) where.name = Like(`%${name}%`); if (status) where.status = status; - 多个字段 OR 关系:用数组,
where: [{ name: Like('%a%') }, { email: Like('%a%') }] - 范围/空值混合判断:用内置操作符,
age: Between(18, 60)、deletedAt: IsNull(),它们都经 TypeORM 参数化处理 - 避免
JSON.stringify()或toString()后再塞进Raw—— 这类转换极易漏掉转义,且破坏参数绑定
QueryBuilder 是最后防线,但必须全程参数化
当 where 对象无法满足逻辑(比如跨表字段计算、窗口函数、复杂 OR 嵌套),才上 QueryBuilder。它的安全边界在于:所有用户输入必须通过 .setParameter() 注入,不能出现在 .where() 字符串里。
- ✅ 正确:
qb.where(`${alias}.status = :status`, { status }) - ❌ 错误:
qb.where(`${alias}.status = '${status}'`)—— 单引号 + 插值,等同于裸拼 - ⚠️ 特别注意:别在
.where()中拼接整个条件字符串,例如.where(getWhereClause(userInput)),除非getWhereClause内部也只返回带:param占位符的字符串,并配合同步的setParameter - 复杂动态条件建议拆成多个
.andWhere()调用,每个都独立 setParameter,比堆在一个字符串里更可控
真正容易被忽略的点是:开发时本地环境常关 synchronize: true,但上线后靠 migration 管理 schema —— 这和注入无关,但一旦你为“快速验证”临时开了 synchronize 又手写了原始 SQL,就可能在测试阶段埋下未被发现的拼接路径。防注入不是加一道开关,而是让每条数据流从 controller 输入开始,就始终走 TypeORM 的参数化通道。











