sql注入在预编译阶段被拦截,因参数与sql文本走独立通道:语句经解析器生成执行计划,参数仅执行时填入内存缓冲区,不参与语法树构建。

SQL注入为什么在预编译阶段就被拦住了
因为数据库引擎根本不会把参数值当SQL代码去解析。你传进来的 user_input,哪怕内容是 ' OR 1=1 --,它也只被当作一个字符串字面量塞进执行计划的占位符里,不会参与语法树构建。
关键在于:SQL语句文本和参数数据走的是两条完全独立的通道——前者送进解析器生成执行计划,后者等执行时才填入内存缓冲区。这和拼接字符串后整条提交给数据库有本质区别。
PreparedStatement 的参数绑定到底做了什么
以 JDBC 为例,调用 setString(1, userInput) 并不修改 SQL 字符串本身,而是把值存进 JDBC 驱动内部的一个参数数组,并标记该位置为“待绑定”。真正发往数据库的是两段二进制数据:prepare 指令附带原始 SQL(不含变量),以及后续 execute 指令附带的参数序列化值。
- MySQL 协议中,参数通过
COM_STMT_EXECUTE包发送,字段类型、长度、值都单独编码,服务端按协议直接解包到对应变量内存地址 - PostgreSQL 使用
Bind+Execute两步,参数值以二进制格式传入,服务端跳过词法分析直接映射到ParamListInfo - SQL Server 的
sp_executesql也是同理,参数列表和 SQL 模板分离传输
哪些写法看似参数化,实则仍危险
常见误判点:只要用了 ? 或 :name 就安全?不是。真正的分水岭在于「参数是否参与 SQL 文本构造」。
- 拼接表名/列名:
"SELECT * FROM " + tableName + " WHERE id = ?"——tableName是字符串拼接,?仅保护了WHERE值,依然可注入 - 动态
ORDER BY:"ORDER BY " + sortColumn + " " + direction—— 参数无法替代标识符,必须白名单校验 - MyBatis 中误用
${}:SELECT * FROM user WHERE status = ${status}—— 这是字符串替换,不是参数绑定 - ORM 的原生查询接口,如 Django 的
extra(where=[...])或 SQLAlchemy 的text(),若未显式用bindparam,仍可能绕过参数化
为什么存储过程里用 EXEC(@sql) 也不行
SQL Server 的 EXEC(@sql) 或 PostgreSQL 的 EXECUTE '...' USING ... 看似带参数,但前提是那条 SQL 字符串本身不能由用户输入拼成。一旦 @sql 是拼出来的,参数再安全也无济于事——因为整个语句结构已经失控。
真正安全的写法是:SQL 模板硬编码,所有动态部分仅来自 USING 后的参数或白名单枚举值。例如 EXECUTE 'SELECT * FROM users WHERE role = $1' USING user_role; 是安全的;而 EXECUTE 'SELECT * FROM ' || table_name || ' WHERE id = $1' USING id; 不安全——table_name 参与了 SQL 构造。
底层没变:只要用户控制的部分进入了词法分析环节,就等于把解析权交了出去。











