参数化查询能防sql注入,因其将sql结构与用户数据严格分离:数据库先编译语句骨架,再把参数作为纯字面量填入,使' or 1=1 --等恶意输入无法改变语法逻辑;但表名、列名、order by等结构部分不可参数化,须白名单校验。

pg 库本身不自动防注入,安全完全取决于你是否正确使用参数化查询——拼错一个字符、用错一个位置,就等于把数据库大门钥匙交出去。
必须用 $1 占位符,不能用模板字符串插值
常见错误是写成 `SELECT * FROM users WHERE id = ${id}`,哪怕 id 是数字类型,只要进了模板字符串,就失去所有防护。PostgreSQL 协议不解析 JS 模板,它只看到最终拼出的字符串,攻击者输入 1 OR true-- 就直接执行。
- ✅ 正确写法:
client.query('SELECT * FROM users WHERE id = $1', [id]) - ❌ 错误写法:
client.query(`SELECT * FROM users WHERE id = ${id}`)或client.query('SELECT * FROM users WHERE id = ' + id) - 占位符
$1必须原样保留,不能被 JS 字符串操作修改;驱动靠识别$n格式来触发参数绑定,不是靠内容语义
IN 子句必须动态生成占位符,不能拼接数组字符串
用户传 [1, 2, 3],不能写成 WHERE id IN ('1','2','3')——引号和逗号全是用户可控的,3', DROP TABLE accounts-- 就能闭合并注入。
- ✅ 安全做法:用
Array(ids.length).fill('$1').join(',')生成占位符串,再用ids数组整体传入参数位 - ❌ 空数组不处理会报错:PostgreSQL 不接受
WHERE id IN (),需显式拦截,如ids.length === 0 ? 'WHERE 1=0' : `WHERE id IN (${placeholders})` - ⚠️ 超长列表(如 >65535 项)会触发 PostgreSQL 参数上限,应提前分批,而不是等驱动抛异常
表名、列名、ORDER BY 字段不能参数化,必须白名单校验
占位符只适用于值(WHERE 右侧、VALUES 列表),不适用于语法结构。试图写 SELECT * FROM $1 或 ORDER BY $1 会直接报错:ERROR: syntax error at or near "$1"。
- ✅ 动态排序字段必须硬编码白名单:
const allowedSort = ['name', 'email', 'created_at']; const sort = allowedSort.includes(req.query.sort) ? req.query.sort : 'id' - ❌ 用正则“过滤单引号”或“只允许字母数字”不可靠——攻击者可用 Unicode 同形字、空字节绕过,白名单才是唯一可信方式
- 同理,
JOIN表名、GROUP BY字段、UNION子句来源,全部适用同一规则
确认驱动没开多语句,否则 ; 就是后门
PostgreSQL 协议支持多语句,但 pg 驱动默认禁用。一旦开启(比如用了非官方 fork 或自定义封装),攻击者输 admin'; DROP TABLE users-- 就真能删表。
- ✅ 验证方法:手动执行
client.query('SELECT 1; SELECT 2'),应报错Multiple statements detected - ❌ 若返回两行结果,说明驱动已被魔改,或启用了危险选项(如某些封装库的
allowExitOnMultipleStatements) - ⚠️ 注意:错误信息里带
syntax error at or near ";"是数据库层报的,说明驱动已把多语句发过去了——这是严重破防信号
最常被忽略的点是:所有动态 SQL 结构部分(表名、列名、排序方向、分组依据)永远无法靠参数化保护,白名单不是“可选优化”,而是强制前提;而 IN 子句的占位符生成逻辑,一旦漏掉空数组或长度截断,线上服务就会在某个凌晨三点突然崩掉。











