前端用 Prepared Statement 能防 SQL 注入吗?

东方晓萌 阅读 18

我最近在学安全防护,看到说用 Prepared Statement 可以防止 SQL 注入。但我是在写前端代码(比如用 fetch 发请求),那我在前端拼 SQL 字符串然后发给后端,是不是照样会被注入?

比如我这样写:const query = SELECT * FROM users WHERE id = ${userId};,就算后端用了 PreparedStatement,前端这么拼接是不是还是有风险?

是不是必须让后端自己处理参数绑定,前端只传参数不能传整条 SQL?

我来解答 赞 11 收藏
二维码
手机扫码查看
1 条解答
Dev · 甜茜
你这个问题问得很到位,说明你已经开始思考安全边界的问题了,这比很多人强多了。

先说结论:前端拼 SQL 字符串,哪怕后端用了 PreparedStatement,只要前端能传完整的 SQL 片段或语句,就依然有注入风险。

原理是这样:PreparedStatement 防注入的关键在于「参数和语句分离」,数据库在预编译阶段就把 SQL 结构固定死了,后面传进来的参数只当作纯数据处理,不会被当成 SQL 语法解析。但前提是:SQL 结构必须由后端完全控制,参数只作为占位符传入。

你举的例子:

const query = SELECT * FROM users WHERE id = ${userId};


如果这段代码在前端执行,然后把整个 query 字符串通过 fetch 发给后端,后端再直接执行(哪怕用了 PreparedStatement,但如果你是把整个 query 字符串当作一个参数传进去,比如 stmt = conn.prepareStatement(query) 这种用法),那 PreparedStatement 就形同虚设了——因为 SQL 结构已经在前端被拼死了,攻击者可以构造像 userId = "1 OR 1=1" 或者更狠的 userId = "1; DROP TABLE users; --" 这样的输入,直接把恶意 SQL 注进去。

举个极端点的例子:假设你前端这么发请求:

const userId = "1; DELETE FROM users; --";
const query = SELECT * FROM users WHERE id = ${userId};
fetch('/api/getUser', {
method: 'POST',
body: JSON.stringify({ sql: query })
});


后端如果这样处理:

// 假设是 Node.js + mysql2
const sql = req.body.sql;
const [rows] = await connection.execute(sql); // 直接执行字符串,不是预编译!


那完蛋,DELETE 语句真就执行了。

再退一步,就算后端用了 PreparedStatement,但你传的是整个 query 字符串作为参数:

const sql = req.body.sql; // 比如是 "SELECT * FROM users WHERE id = ?"
const stmt = await connection.prepare(sql); // 这里 prepare 的是整个字符串
await stmt.execute([userId]); // userId 是参数


这看起来像用了 PreparedStatement,但问题在于:SQL 语句结构本身是前端决定的,攻击者可以传 "SELECT * FROM users WHERE id = ? OR 1=1",然后参数传个 "1",结果还是被绕过了。

所以正确的做法是:

前端只传参数,别传 SQL 结构;后端固定 SQL 模板,用占位符绑定参数。

比如后端写死这个 SQL:

SELECT * FROM users WHERE id = ?


前端只发:

{ "userId": "123" }


后端收到后,用这个固定模板去预编译,把 userId 当作参数传进去。这样哪怕 userId 是 "1 OR 1=1",它也只会被当成字符串值去比对,不会被当 SQL 执行。

用代码演示一下正确姿势:

前端(比如 React / Vue / 原生 JS 都一样):

const userId = document.getElementById('input').value; // 用户输入的值,比如 "123"
fetch('/api/user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: userId }) // 只传参数,不拼 SQL
});


后端(Node.js + mysql2 为例):

app.post('/api/user', async (req, res) => {
const { id } = req.body;

// 后端固定 SQL 模板,用 ? 占位符
const sql = 'SELECT * FROM users WHERE id = ?';

// 用预编译 + 参数绑定,确保 id 被当作数据处理
const [rows] = await connection.execute(sql, [id]);

res.json(rows);
});


这样不管前端传什么,id 都不会被当作 SQL 语法解析。哪怕有人传 id = "1 OR 1=1",数据库执行的其实是:

SELECT * FROM users WHERE id = '1 OR 1=1'


它只是查 id 字段等于这个字符串的记录,根本不会触发 OR 逻辑判断。

再强调一遍:SQL 预编译只在后端生效,前端永远不该知道数据库结构,更不该参与 SQL 拼接。前端只负责传「数据」,后端负责「语句」。

顺便吐槽一句:很多初学者以为用了 ORM 就万事大吉,但其实如果 ORM 的 query 构造也是前端传字符串,或者用了 raw() / exec() 这类动态执行方法,一样有风险。安全这事,得从最底层的通信协议设计开始就分清边界。

最后再补一句:如果你的后端 API 是给前端调用的,那它就该是「受保护的内部接口」,不是「给用户直接写 SQL 的入口」。真要搞复杂查询,也该由后端暴露「带参数的查询接口」,比如 /api/users?role=admin&status=active,后端解析这些参数去构造 SQL,而不是让用户传 SQL 字符串回来。

明白了吗?要是还有疑问,比如用 Java / PHP / Python 的例子,我可以再给你补几个语言的代码。
点赞
2026-02-27 05:00