什么是SQL注入
SQL 注入是最常见的 Web 安全漏洞之一。 当后端程序将用户输入直接拼接进 SQL 语句时,攻击者可以构造恶意输入,改变原本的 SQL 逻辑,从而绕过验证、读取敏感数据,甚至篡改数据库内容。
举例:有一个登录验证的 SQL:
SELECT * FROM users WHERE username = 'admin' AND password = '123456';
如果代码直接拼接字符串:
sql = "SELECT * FROM users WHERE username = '" + userInput + "' AND password = '" + pwdInput + "'";
如果攻击者构造一个恒真条件(如 OR 1=1)`,而密码随便写。就会变为:
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = 'xxx';
导致绕过验证,直接登录成功。
SQL常见注入方式
-
基于字符串拼接的注入(最常见)。
-
布尔盲注:根据返回结果是否不同判断条件真假。
-
时间盲注:利用 sleep() 或延迟函数判断真假。
-
报错注入:利用数据库报错信息提取数据。
-
联合查询注入:利用 UNION SELECT 拼接结果获取其他表数据。
解决方法
- 使用预编译语句(PreparedStatement)/参数化查询:
这是解决该问题最有效的方案,不拼接字符串,而是用参数绑定。
举例:
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
执行流程:
- SQL 结构先被数据库预编译
- 用户输入作为参数绑定
- 输入数据不会被当作 SQL 结构解析
这样即使输入了 OR '1'='1 也会被当作普通字符串处理,不会执行注入。
- 使用ORM框架:MyBatis、Hibernate
ORM 框架一般都会内置参数化机制,避免拼接 SQL。
MyBatis是基于JDBC的持久层框架,本质上还是用JDBC来执行SQL。因此这里先讲JDBC的措施:
仍然是使用预编译:依旧是上面代码的例子,这里的 ? 是占位符,JDBC 会先把 SQL 结构预编译,再把用户输入作为 参数绑定,不会当作 SQL 语句拼接,所以能有效防止注入。
而MyBatis中:
如果使用 ${} 就容易字符串拼接:
<select id="getUser" resultType="User">
SELECT * FROM users WHERE username = '${username}'
</select>
此时若传入 admin' OR '1'='1,就会拼接到 SQL 里,存在注入风险。
而采用 #{ } 来进行参数占位,就是安全的:
<select id="getUser" resultType="User">
SELECT * FROM users WHERE username = #{username}
</select>
这里的 #{} 底层会用 PreparedStatement 的 ? 占位符处理,相当于参数化查询,能防止注入。
而这里就会引出:
${}和#{}的区别:
#{} —— 预编译参数占位符
会被替换为 ?,底层用 PreparedStatement 来执行。此时SQL结构会被预编译,而用户输入会作为参数绑定,而不是直接拼接到SQL字符串中。【有效防止注入】
特点:
- 会被替换为 ?
- 使用 PreparedStatement
- 参数安全绑定
- 可以有效防止 SQL 注入
绝大多数查询、插入、更新都应该使用 #{}
${} —— 字符串直接拼接
直接把参数值拼接到 SQL 里。类似于 JDBC 的 Statement,不会做预编译。【容易造成 SQL 注入】
特点:
- 直接拼接字符串
- 类似 JDBC 的 Statement
- 存在 SQL 注入风险
实际应用场景
#{}:绝大多数场景都应该用 #{},比如 条件查询、插入、更新。
${}:只有在 动态拼接 SQL 关键字时才用,比如表名、列名不能用 ? 占位,只能拼接:
<select id="getData" resultType="Map">
SELECT ${column} FROM ${table} WHERE id = #{id}
</select>
但这种写法要小心,必须配合 白名单校验,确保 column/table 只能是你允许的范围,否则会被注入。
总结:
- #{} 会被替换为 ?,底层用 PreparedStatement,参数会被安全绑定,可以防止 SQL 注入,一般查询和更新都用它。
- ${} 是字符串拼接,类似于 Statement,会直接把参数拼进去,有 SQL 注入风险,通常只在动态拼接表名、列名等场景下用,而且需要白名单过滤。