必须用$1、$2占位符配数组传参,禁止?或字符串拼接;动态字段名/排序方向须白名单校验;禁用pg.format();错误响应和日志不得暴露sql或用户输入。

必须用 $1、$2 占位符配数组传参,不能混用 ? 或字符串拼接,否则参数化失效。
pg.query() 必须传两个参数:SQL 字符串 + 值数组
pg 驱动只认 $n 形式占位符,底层靠 libpq 绑定参数。写成 WHERE name = ? 或 WHERE name = ' + req.query.name 都会直通 SQL 解析器,等于裸奔。
- ✅ 正确:
client.query('SELECT * FROM users WHERE id = $1 AND status = $2', [req.params.id, req.query.status]) - ❌ 错误:
client.query('SELECT * FROM users WHERE id = ?', [req.params.id])(?是 mysql2 的语法) - ❌ 错误:
client.query(`SELECT * FROM users WHERE name = '${req.query.name}'`)(f-string 拼接,100% 可注入)
动态字段名/排序方向必须白名单校验
只能替换值,不能替换列名、表名、ORDER BY 后的字段或 ASC/DESC。这些属于 SQL 结构部分,数据库不允许参数化。
一款AI工具,主要用于一款基于 Rust 的快速无头浏览器自动化命令行工具(CLI),支持 Node.js 回退机制,可使 AI agent 通过结构化命令实现页面导航、点击、输入及截图,适合需要提升相关任务效率的用户。
- 排序字段:定义
const allowedSortFields = ['created_at', 'email', 'role'],再检查req.query.sort是否在其中 - 排序方向:只接受
req.query.order === 'ASC' || req.query.order === 'DESC',其余直接return res.status(400).end() - 禁止用正则过滤字段名(如
/^[a-z_]+$/i),攻击者可用user_name\0或 Unicode 变体绕过
别碰 pg.format() —— 它不是参数化,是危险的字符串模板
pg.format() 是纯客户端格式化函数,不走 libpq 参数绑定流程。它只是把值插进字符串里,和 util.format() 本质一样。
- ❌ 危险:
pg.format('SELECT * FROM %I WHERE id = %L', tableName, req.params.id)——%I和%L仍可能被绕过,且无法防御所有编码变体 - ✅ 替代方案:表名/字段名全部硬编码,或从预设映射对象取值,例如
const tables = { users: 'users', logs: 'admin_logs' },再校验req.query.table是否为tables[req.query.table]
错误响应和日志里绝不能出现原始 SQL 或用户输入
即使你 100% 用了 $1,只要在 catch 块里写 console.error(err) 或返回 { error: err.message },就可能泄露表结构、字段名甚至完整 SQL,帮攻击者构造下一轮 payload。
- 日志中:用固定消息代替原始错误,如
console.error('DB query failed for user lookup'),不拼接req.query - HTTP 响应:统一返回
500 Internal Server Error,不带任何数据库细节;开发环境可开pg的error.stack,但生产必须关 - 特别注意:ORM 如 Prisma 若调用
$queryRaw,同样要遵守白名单规则,不能把req.query.sort直接塞进去
真正难的不是写对 $1,而是守住所有“非值位置”——字段名、表名、GROUP BY、UNION SELECT 子句、甚至 JSONB 路径表达式。这些地方没有占位符,只能靠硬编码或白名单,少一个校验点,前面所有参数化都归零。










