🔒 安全第一

SQL 注入 防护

用户提供的值不会被拼接进 SQL 文本——通过参数占位符绑定(随方言不同,例如 $1 或 ?)

100%
参数绑定
所有用户值使用占位符
0
值拼接
用户数据不出现在 SQL 文本中
保证
顺序保留
占位符与参数 1:1 对应
137
安全测试
注入尝试覆盖(当前套件)

理解 SQL 注入

SQL 注入仍然是最关键的 Web 应用漏洞之一

当不受信任的数据被作为 SQL 文本的一部分而不是通过安全的参数绑定传入时,就会发生 SQL 注入。攻击者可能改变查询语义,从而访问未授权数据、修改记录或触发非预期操作。

常见攻击模式

身份验证绕过

admin' OR '1'='1

通过使 WHERE 条件恒为真来尝试绕过登录

数据外泄

' UNION SELECT password FROM users--

通过 UNION 尝试从其他表提取数据

破坏性命令

'; DROP TABLE users; --

尝试追加额外语句以删除数据

参数顺序错配滥用

利用:如果占位符与参数错配,权限检查可能使用错误的值

示例:WHERE userId=$1 AND isAdmin=$2,但 params=[true, 123] 而不是 [123, true] 可能错误授予管理员权限

多层防护策略

每个查询都会经过多层安全校验

第1层:自动参数绑定

所有用户值会被转换为绑定参数。用户值不会作为字面量进入 SQL 文本。

  • 用户值使用方言相关的占位符表示(例如 $1 或 ?)
  • 数组输入会被参数化(展开为多个占位符或作为数组参数,取决于方言/策略)
  • NULL 与可选值在不拼接用户输入到 SQL 文本的情况下处理
  • 日期/时间值参数化
  • JSON 值参数化

第2层:字段名校验

在生成查询之前,所有字段名都会根据 Prisma schema 元数据校验。

  • 只允许 schema 中定义的字段
  • 关系过滤基于 schema 元数据校验
  • 不接受任意字段名
  • 在适用处拒绝不支持/计算字段
  • 拒绝原型污染载荷(例如 __proto__、constructor)

第3层:标识符净化

表名、列名与别名会被校验,并在需要时按方言安全加引号/转义。

  • 拒绝控制字符
  • 保留关键字在需要时加引号
  • 在配置时支持 schema 限定引用
  • 标识符按方言规则转义(例如双引号转义)
  • 按方言规则强制标识符最大长度

第4层:操作符校验

每种字段类型仅允许已知的安全操作符。

  • 字符串操作符:contains、startsWith、endsWith
  • 数值操作符:lt、lte、gt、gte
  • 数组操作符:in、notIn(带类型校验)
  • 逻辑操作符:AND、OR、NOT(结构校验)
  • 未知操作符立即拒绝

第5层:参数顺序保证

严格顺序确保每个占位符位置都映射到对应参数,避免错配。

  • 顺序计数器防止重新排序
  • SQL 构建与参数数组同步构造
  • 没有中间缓冲或重新排序步骤
  • 测试验证:生成的 SQL 与参数始终一致
  • 一对一对应:位置 N → params[N-1](或该方言的等价映射)

形式化安全保证

在明确设计前提下的抗注入证明概要(参数绑定、无 raw-SQL 绕过、驱动正确使用)

定理1:值隔离

对于所有用户提供的值 V,不存在执行路径使 V 作为 SQL 文本出现在生成的查询字符串中。

按构造,所有处理用户值的代码路径都会调用 addParameter(value),它: 1. 将值存储在独立的 params 数组中 2. 返回用于 SQL 文本的占位符 token 3. 只有占位符 token 会拼入 SQL 字符串 4. 数据库驱动分别接收 SQL 文本与参数值 ∴ 用户值不会被当作 SQL 语法解析

const addParameter = (params: any[], value: any) => {
  params.push(value)
  const index = params.length
  return `$${index}`
}

定理2:字段名封闭性

任何生成查询中的字段名集合 F 都是 schema 定义字段集合 S 的子集,即 F ⊆ S。

对每一次字段引用: 1. 从查询对象中提取字段名 2. 按 schema 元数据查找 3. 若字段不在 schema 中则抛错 4. 只有通过校验的字段才能进入 SQL 生成 ∴ 任意字段名不能出现在 SQL 中

const validateField = (field: string, model: Model) => {
  if (!model.fields.has(field)) {
    throw new Error(`Field ${field} does not exist`)
  }
  return model.fields.get(field)
}

定理3:操作符安全

对字段 F 应用的任意操作符 O,都必须属于字段类型 T 允许的操作符集合。

操作符校验算法: 1. 从 schema 获取字段类型 T 2. 定义 allowed_ops(T) = { 类型 T 允许的操作符 } 3. 对查询中的操作符 O: - 若 O ∉ allowed_ops(T),抛错 - 否则以参数绑定方式应用操作符 ∴ 只能使用类型匹配的操作符

const ALLOWED_OPS: Record<string, string[]> = {
  String: ['contains', 'startsWith', 'endsWith'],
  Int: ['lt', 'lte', 'gt', 'gte']
}
if (!ALLOWED_OPS[fieldType]?.includes(operator)) {
  throw new Error('Invalid operator')
}

定理4:标识符安全

所有 SQL 标识符 I 都会校验以排除控制字符,并按方言规则进行安全转义与加引号。

标识符处理: 1. 若包含控制字符则拒绝 2. 按方言规则转义内部引号 3. 在需要时加引号(保留关键字或包含特殊字符) 4. 按方言规则应用标识符长度限制 ∴ 标识符不能破坏 SQL 语法或注入 token

const quoteIdentifier = (id: string) => {
  if (/[\x00-\x1F]/.test(id)) {
    throw new Error('Invalid characters')
  }
  const escaped = id.replace(/"/g, '""')
  const needsQuoting = /[^a-z0-9_]/i.test(id) || isReservedKeyword(id)
  return needsQuoting ? `"${escaped}"` : id
}

定理5:参数顺序一致性

对 SQL 文本中出现的每个占位符位置 N,params[N-1] 都是该位置预期的确切值,不会发生重排或错配。

参数顺序保证: 1. 单一追踪器维护插入顺序 2. 每次 add() 都先追加值到 params,再立刻返回下一占位符 3. SQL 构建与 params 构建同步进行 4. 不存在中间重排步骤 ∴ 一对一映射始终成立

class ParameterTracker {
  private params: any[] = []

  add(value: any): string {
    this.params.push(value)
    return `$${this.params.length}`
  }

  getParams(): any[] {
    return this.params
  }
}

const tracker = new ParameterTracker()
const sql = `WHERE email = ${tracker.add(email)} AND age > ${tracker.add(age)}`
const params = tracker.getParams()

全面的安全测试覆盖

137 个测试验证对常见 SQL 注入向量与边缘情况的防护(当前套件)

值参数化 (basic.test.ts)

  • 带引号的字符串:user'with'quotes
  • 带分号的字符串:user;extra
  • 作为值的 SQL 关键字:DROP TABLE users
  • 复杂注入:'; DROP TABLE users; --
  • Union 攻击:' UNION SELECT * FROM users--
  • Boolean 攻击:admin' OR '1'='1
  • 注释注入:test@example.com' -- comment

字段名验证 (identifiers.test.ts)

  • 拒绝 SELECT 中的恶意字段名
  • 拒绝 WHERE 中的恶意字段名
  • 拒绝 ORDER BY 中的恶意字段名
  • 拒绝字段名中的 SQL 注入
  • 拒绝不存在的字段
  • 拒绝原型污染:__proto__、constructor

LIKE 模式安全 (like-patterns.test.ts)

  • 通配符注入:%' OR '1'='1
  • 下划线注入:test_' OR '1'='1
  • 反斜杠处理:test\\'; DROP--
  • 多个通配符:%_%'; DROP--
  • 不区分大小写注入:'; UNION SELECT--

数组操作符安全 (array-operators.test.ts)

  • 带恶意数组的 IN:['; DROP--', 'UNION SELECT--']
  • 带注入的 NOT IN:['; TRUNCATE--', 'DELETE FROM--']
  • 空数组处理
  • 大数组验证(100+ 项)
  • 混合类型数组处理

边缘情况 (edge-cases.test.ts)

  • Unicode 注入:\u0027 OR \u00271\u0027=\u00271
  • 十六进制编码注入:0x31=0x31--
  • URL 编码:%27%3B%20DROP%20TABLE
  • 堆叠查询:'; DROP TABLE users; SELECT
  • 基于时间的盲注(方言相关):WAITFOR DELAY '00:00:05'--
  • 基于 UNION:UNION ALL SELECT null, password
  • 二阶注入尝试

参数顺序验证 (basic.test.ts)

  • 占位符位置顺序一致
  • 参数数组与占位符顺序匹配
  • 复杂查询在条件间保持顺序
  • 嵌套 OR/AND 条件保持参数序列
  • 关系过滤保持正确的参数映射

安全与不安全:代码比较

❌ 不安全:字符串拼接

const email = req.body.email
const sql = "SELECT * FROM users WHERE email = '" + email + "'"

✅ 安全:自动参数绑定

const email = req.body.email
const { sql, params } = toSQL('User', 'findMany', {
  where: { email }
})

const result = { sql, params }

实现细节

参数管理

集中式参数跟踪确保每个用户值都被绑定且顺序稳定

class ParameterTracker {
  private params: any[] = []

  add(value: any): string {
    this.params.push(value)
    return `$${this.params.length}`
  }

  getParams(): any[] {
    return this.params
  }
}

字段验证

基于 schema 的验证防止任意字段访问

function validateField(
  fieldName: string,
  model: ModelInfo
): FieldInfo {
  const field = model.fields.get(fieldName)
  if (!field) {
    throw new Error(
      `Field "${fieldName}" does not exist`
    )
  }
  return field
}

标识符加引号

方言感知的标识符校验与加引号可防止通过标识符触发的语法注入

function quoteIdentifier(identifier: string): string {
  if (/[\x00-\x1F]/.test(identifier)) {
    throw new Error('Invalid control characters')
  }

  const escaped = identifier.replace(/"/g, '""')
  const needsQuoting = /[^a-z0-9_]/i.test(identifier) || isReservedKeyword(identifier)

  if (needsQuoting) {
    return `"${escaped}"`
  }

  return identifier
}

安全最佳实践

永远不要禁用验证

不要绕过 schema 验证或字段检查。这些是关键的安全层。

Do: 按设计使用库并进行完整验证
Don't: 绕过 schema 验证或将 raw SQL 写入 SQL 文本

保持 Schema 更新

确保您的 Prisma schema 准确反映数据库结构。

Do: 在 schema 更改后运行 prisma db pull 和 prisma generate
Don't: 使用过时的 DMMF 或 schema 定义

验证输入类型

TypeScript 提供编译时安全性,但运行时验证可提供更深一层防护。

Do: 对查询参数使用 Prisma 生成的类型
Don't: 在传递给查询之前将用户输入转换为 any

监控查询模式

在生产中记录并监控生成的 SQL 与参数,以检测异常与误用。

Do: 启用查询日志并审查模式
Don't: 在生产环境中无监控运行

设计即安全

SQL 注入防护内置于库的每一层。请直接查看安全测试与实现细节。