预编译 SQL 为什么能够防止 SQL 注入
预编译 SQL 为什么能够防止 SQL 注入
Diebug前言
最近一位搞网络安全的朋友问我,为什么用 PreparedStatement
预编译的 SQL 就不会有被 SQL 注入的风险?
初听之下,我想到了面试八股文中关于 MyBatis 的 ${}
和
#{}
区别的问题,但再细想其中缘由,却不甚明了。
于是,我决定研究一下:预编译到底是什么?它如何与数据库交互?PreparedStatement
在这一过程中又做了什么?
一、数据库预编译
1. 什么是预编译 SQL?
以 MySQL 为例,所谓预编译是指先提交带占位符的 SQL
模板,数据库为其生成一个 key
并进行编译,随后用户通过
key
和参数让数据库执行完整的 SQL。类似于 Python 的
format
函数。
示例如下:
1 | PREPARE stmt FROM 'SELECT * FROM users WHERE username = ?'; |
最终执行的 SQL:
1 | SELECT * FROM users WHERE username = 'john'; |
2. 原理
预编译的核心在于:SQL 模板在 PREPARE
阶段已完成语法检查与编译,后续传入的参数只会替换占位符,不参与 SQL
的语法解析。例如:
- 将
FROM
后的表名作为占位符:编译失败,因为表名必须在语法检查阶段确定。1
2PREPARE stmt FROM 'SELECT * FROM ? WHERE username = ?';
-- Error: SQL syntax error - 参数化字段:
最终的 SQL:
1
2
3PREPARE stmt FROM 'SELECT ? FROM users';
SET @field = 'username';
EXECUTE stmt USING @field;参数被视为字符串,未作为 SQL 关键字处理。1
SELECT 'username' FROM users;
这说明,在预编译阶段,SQL 模板的结构已定型,后续参数无法改变语法树,避免了 SQL 注入。
二、JDBC 中的预编译
1. PreparedStatement
在 Java 中,JDBC 提供了 PreparedStatement
,它是预编译
SQL 的实现。常见用法如下:
1 | String sql = "SELECT * FROM users WHERE username = ?"; |
其中,setString
等方法负责将参数填充到占位符中。
2. 虚假的“预编译”
默认情况下,MySQL 的 PreparedStatement
使用的是客户端预编译。参数的转义在 Java
层完成,并在执行前将模板与参数拼接为普通 SQL。
示例:
1 | String sql = "SELECT * FROM users WHERE username = ?"; |
数据库日志:
1 | Query SELECT * FROM users WHERE username = 'john'' OR ''1''=''1' |
可以看到,参数已被转义,但最终 SQL 并未使用数据库的预编译功能。
3. 真正的预编译
要启用真正的预编译,可以在 JDBC URL 中设置
useServerPrepStmts=true
:
1 | jdbc:mysql://localhost:3306/test?useServerPrepStmts=true |
启用后:
- 数据库日志显示 SQL 模板和参数分离:
1
2Prepare SELECT * FROM users WHERE username = ?
Execute SELECT * FROM users WHERE username = 'john' - SQL 的编译和参数绑定均由数据库完成,真正实现了预编译。
三、MyBatis 的 #{}
与
${}
1. #{}
的行为
#{}
会将参数解析为占位符 ?
,并通过
PreparedStatement
设置参数值:
1 | SELECT * FROM users WHERE username = ? |
2. ${}
的行为
${}
会直接将参数值嵌入到 SQL 模板中:
1 | SELECT * FROM users WHERE username = 'john' |
这种方式容易导致 SQL 注入问题。
MyBatis 源码验证:
#{}
使用GenericTokenParser
解析为?
,并绑定参数。${}
使用参数直接替换占位符。
3. 为什么 #{}
更安全?
#{}
使用占位符,参数值始终作为数据处理,不会影响 SQL
模板的结构;而 ${}
会直接将参数拼接到 SQL
中,可能破坏原始语法结构。
总结
- 预编译的本质:通过固定 SQL 模板结构,将参数视为数据进行转义,防止注入。
- JDBC 的预编译:默认情况下,MySQL 的
PreparedStatement
为客户端预编译,仅在参数设置时进行转义;设置useServerPrepStmts=true
后,可启用真正的预编译。 - MyBatis 的占位符:
#{}
通过占位符与参数绑定,更安全;${}
直接拼接参数,易受 SQL 注入影响。
最后,尽管预编译能显著降低 SQL 注入风险,但仍需遵循安全开发规范,避免不必要的隐患。