参数化查询分预编译和执行两阶段:第一阶段仅发送SQL模板,数据库解析并固化语法树;第二阶段单独发送参数值,不参与词法与语法分析,彻底阻断SQL注入。

参数化查询的执行流程分两阶段,SQL结构和用户数据不走同一条路
数据库不是先拼字符串再解析,而是把“语句骨架”和“参数值”拆成两个独立通道处理。你调用 cursor.execute("SELECT * FROM users WHERE id = %s", (user_input,)) 时,底层实际发了两条指令:第一条是预编译请求,只含原始 SQL 文本(不含任何变量);第二条是执行请求,只含序列化后的参数值(比如 'admin' OR '1'='1' 被当作一个长度为17的字节流)。数据库在第一阶段就锁死了语法树,第二阶段的参数根本不会触发词法分析——它连单引号都进不了解析器。
占位符不是“替换”,而是运行时内存映射
很多人误以为 %s 或 ? 是字符串替换占位符,其实不是。以 JDBC 的 PreparedStatement 为例:setString(1, userInput) 并不修改 SQL 字符串,只是把值存进驱动内部的参数数组,并标记位置。MySQL 协议中,参数通过 COM_STMT_EXECUTE 包单独发送,字段类型、长度、值全部二进制编码;PostgreSQL 则用 Bind + Execute 两步,参数值以 binary format 直接解包到 ParamListInfo 内存结构里。攻击者输入的 ' OR 1=1 -- 最终只会出现在 WHERE 条件的字符串比较值中,不会参与任何语法重构。
哪些写法看似参数化,实则完全失效
只要用户输入参与了 SQL 文本构造,就等于没防住。这类错误非常隐蔽,因为代码里确实出现了 ? 或 %s:
-
"SELECT * FROM " + tableName + " WHERE id = ?"——tableName是字符串拼接,?只保护了值,表名已失控 -
ORDER BY ${sortField} ${direction}—— JavaScript 模板字符串直接拼接,sortField可传id; DROP TABLE users-- - MyBatis 中写
SELECT * FROM user WHERE status = ${status}——${}是字符串替换,#{}才是参数绑定 - Prisma 的
$queryRawUnsafe或 Django 的extra(where=[...])—— 这些 API 明确绕过参数化管道
参数化只保“值”,不保“结构”,动态标识符必须白名单校验
列名、表名、GROUP BY、ORDER BY、LIMIT 偏移量等,数据库协议根本不支持参数化。你不能写 SELECT * FROM ? WHERE ... 或 ORDER BY ?。这些地方一旦需要动态,唯一安全做法是枚举合法值并做严格匹配:
- 排序字段只允许
['id', 'created_at', 'score'],收到sort=id才拼ORDER BY id - 分表逻辑用
shard_id % 4算出表后缀,硬编码logs_0~logs_3,不拼接变量 - 存储过程中避免
EXEC(@sql),改用EXECUTE 'SELECT * FROM users WHERE role = $1' USING user_role
最容易被忽略的是:参数化查询的安全性完全取决于“是否参与 SQL 文本生成”。哪怕只有一处拼接表名,整条查询链就崩了——因为数据库看到的,从来不是你写的那行 Python 或 Java 代码,而是最终提交给它的那串字节流。

















