pg-promise 强制参数化查询,仅对值使用 ${key} 占位;表名、列名等标识符必须用 pgp.as.name() 安全转义,严禁字符串拼接;键名需白名单校验,数组用 ANY 或 pgp.as.csv() 处理。

pg-promise 本身不提供字符串拼接式 SQL 格式化,也不该被用来“格式化变量”——它强制你走参数化查询路径。直接拼接用户输入进 SQL 字符串(哪怕用 pgp.as.format() 或 pgp.as.text())是高危操作,等同于绕过防护。
下面说清楚怎么做、为什么、以及最容易栽跟头的地方。
pg-promise 的参数化查询必须用命名占位符 ${name}
所有动态值都得通过 ${key} 占位,再传对象或数组进去。数据库驱动层会把值当纯数据处理,不解析为 SQL 语法。
常见错误现象:用字符串模板拼接字段名、表名、ORDER BY 条件,比如 `SELECT * FROM ${tableName}` —— 这根本不会被参数化,pg-promise 会原样代入,导致 SQL 注入。
- ✅ 正确:只对值(WHERE 条件、INSERT 值、LIMIT 数字)用
${value} - ❌ 错误:对标识符(表名、列名、排序字段)也用
${xxx}不加转义 - ⚠️ 注意:
pgp.as.name()才是安全处理标识符的函数,例如pgp.as.name(tableName)
标识符(表名/列名)必须用 pgp.as.name() 处理
字段名、表名、GROUP BY 列这些属于 SQL 结构的一部分,不能靠参数化保护。必须显式调用 pgp.as.name() 做双引号包裹和转义。
使用场景:动态选择表、按不同字段排序、构建 UNION 查询等。
-
pgp.as.name('users')→"users" -
pgp.as.name(['user', 'name'])→"user"."name" - 若传入不可信的变量(如 URL query 参数),先白名单校验再进
pgp.as.name(),别跳过校验
不要用 pgp.as.format() 拼接 SQL 主体
pgp.as.format() 是个“高级工具”,但绝大多数业务场景不该碰它。它只是字符串替换,不区分值与结构,且不经过 PostgreSQL 协议层的参数绑定。
使用约定式提交(Conventional Commits)生成标准化的 Git 提交信息。适用于以下场景: (1) 暂存更改时需要提交信息; (2) 审查 diff 时希望获得结构化摘要; (3) 编写 PR 描述或变更日志(changelog)条目; (4) 用户明确要求“为本次更改编写提交信息”。 该功能会分析已暂存/未暂存的 diff,并生成按类型(type)划分范围的提交信息。
容易踩的坑:
- 传入
{table: 'users; DROP TABLE users;--'},pgp.as.format('SELECT * FROM ${table}', values)会照常执行恶意语句 - 开发者误以为它和
${key}占位一样安全,其实完全不是一回事 - 性能上无优势,还增加维护复杂度
真正需要它的极少数情况:生成 DDL(如 CREATE TABLE)、动态视图定义,且已严格校验输入来源。
数组参数和 IN 查询要配 $1, $2… 或 pgp.as.csv()
PostgreSQL 不支持 WHERE id IN (${ids}) 直接展开数组。硬写会变成 WHERE id IN ('1,2,3'),类型错、逻辑错。
正确做法有两种:
- 用预编译数组参数:
WHERE id = ANY($1::int[]),然后传[1,2,3]作为单个参数 - 用
pgp.as.csv()生成逗号分隔列表:WHERE id IN (${pgp.as.csv(ids)}),此时ids必须是数字或字符串数组,且已清洗过
注意:pgp.as.csv() 输出的是 SQL 文本片段,本质仍是字符串拼接,所以仅限用于值列表,绝不能用于字段或表名。
最常被忽略的一点:即使用了 ${key},如果 key 名来自用户输入且没做白名单过滤(比如把 req.query.sortBy 直接当 key 传进查询对象),攻击者仍可通过构造 sort=updated_at; DROP TABLE users-- 触发注入——因为 pg-promise 只校验值,不校验键名。键名必须提前约束范围,例如 const allowedSorts = ['name', 'email', 'created_at']; if (!allowedSorts.includes(sortBy)) throw ...。

















