Next.js API路由中必须使用参数化查询防止SQL注入,禁用字符串拼接;PostgreSQL用client.query(text, params),MySQL用execute(),SQLite用?占位符;Prisma虽默认安全,但动态字段名、raw()调用及错误泄露仍存风险。

Next.js API路由里直接拼接SQL字符串一定会出事
别用 req.query.id 或 req.body.name 直接插进 SQL 字符串里,哪怕加了 parseInt() 或正则过滤也不保险。PostgreSQL 的 UNION 注入、MySQL 的宽字节绕过、SQLite 的注释截断……这些在服务端 API 路由里都真实存在,而且 Next.js 默认不带任何 SQL 安全层。
真正能防住的只有参数化查询(parameterized query)——数据库驱动自己处理值的转义和类型绑定,不是靠你写正则或 escape() 函数。
- 用
pg时必须走client.query(text, params)形式,text是纯 SQL 字符串(不含变量),params是数组或对象 - 用
mysql2时必须用connection.execute(sql, params),不能用query(sql)拼接 - Prisma 是默认安全的,所有
.findUnique()、.where()都走参数化,但要注意别手动拼raw()
PostgreSQL + pg 示例:怎么写才不被绕过
假设你有个 GET /api/users?id=123,后端用 pg 查用户。下面这种写法是错的:
// ❌ 危险!字符串拼接
const id = req.query.id;
const { rows } = await client.query(`SELECT * FROM users WHERE id = ${id}`);正确写法必须分离 SQL 结构和数据:
// ✅ 安全:参数化查询
const id = parseInt(req.query.id as string, 10);
if (isNaN(id)) throw new Error('Invalid ID');
const { rows } = await client.query('SELECT * FROM users WHERE id = $1', [id]);-
$1是占位符,[id]是参数数组,pg 驱动会把id当整数绑定,不会解析成 SQL 片段 - 即使传
?id=1; DROP TABLE users;,最终执行的也只是WHERE id = 1,分号后的内容被完全忽略 - 如果要用命名参数(可读性更好),写成
WHERE id = $(id)并传对象{ id }也行,但注意pg默认只支持位置参数
MySQL 和 SQLite 的关键差异点
MySQL 的 mysql2 默认开启 multipleStatements: false,这能防基础的语句堆叠(如 1; DROP TABLE),但不能替代参数化。SQLite 的 sqlite3 模块同样要求用 db.get(sql, params),而不是 db.get(sql.replace('?', id))。
-
mysql2中,execute()是唯一推荐方式;query()即使开了mysql2.escape()也容易漏掉嵌套 JSON 或时间格式 - SQLite 不支持命名参数,一律用
?占位,顺序必须和数组严格对应:db.all('SELECT * FROM logs WHERE level = ? AND ts > ?', ['error', Date.now() - 3600000]) - 所有驱动都要求你在连接池初始化时显式设置类型映射(比如把
int4映射为 JS number),否则可能返回字符串导致后续逻辑误判
Prisma 里哪些操作看似安全实则危险
Prisma 本身是安全的,但开发者常在 where 条件里混入原始 SQL 或动态键名,这就破防了:
// ❌ 危险:拼接字段名,绕过 Prisma 参数化
const field = req.query.field; // 来自用户输入
const users = await prisma.user.findMany({
where: { [field]: req.query.value } // 如果 field = '__proto__' 或 'OR 1=1' 就出问题
});- 字段名、表名、排序方向(
orderBy)都不能来自用户输入,必须硬编码或白名单校验 -
prisma.$queryRaw和prisma.$executeRaw是“裸 SQL”入口,一旦用就等于放弃 Prisma 的防护,必须自己做参数化(且只接受Prisma.sql模板字面量) - 如果必须动态字段,先查白名单:
const allowedFields = ['name', 'email', 'status']; if (!allowedFields.includes(field)) throw 400;
最易被忽略的是错误信息泄露:别把 pg 原始错误堆栈直接 res.json(err) 返回给前端,里面可能含表结构、字段名甚至部分 SQL —— 至少用 err.message.replace(/.*?WHERE.*?/g, '[REDACTED]') 过滤一下。



















