动态SQL被SQL注入利用的原理与场景
动态SQL是指在程序运行过程中拼接生成的SQL语句,常见于根据用户输入的不同条件组合查询逻辑的场景。当开发者直接将用户输入的内容拼接到SQL字符串中,没有做任何安全处理时,攻击者就可以构造特殊输入篡改SQL原本的逻辑,这就是SQL注入利用动态SQL的核心原理。

典型的漏洞代码示例
以Java语言操作MySQL数据库为例,以下是一段存在风险的动态SQL拼接代码:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
public class DynamicSqlDemo {
public static void main(String[] args) throws Exception {
// 模拟用户输入,正常输入为用户名,恶意输入可构造注入 payload
String userInput = "admin' OR '1'='1";
// 直接拼接用户输入到SQL语句中,存在注入风险
String sql = "SELECT * FROM user WHERE username = '" + userInput + "'";
System.out.println("执行的SQL语句:" + sql);
// 建立数据库连接
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while (rs.next()) {
System.out.println("用户ID:" + rs.getInt("id") + ",用户名:" + rs.getString("username"));
}
rs.close();
stmt.close();
conn.close();
}
}
上述代码中,如果用户输入的是admin' OR '1'='1,最终拼接的SQL会变成SELECT * FROM user WHERE username = 'admin' OR '1'='1',这个条件永远成立,攻击者可以绕过用户名验证获取所有用户数据。
动态SQL注入的常见利用方式
- 绕过身份验证:通过构造恒真条件跳过登录校验逻辑
- 数据窃取:使用
UNION查询拼接其他表的数据,获取敏感信息 - 数据篡改:通过
UPDATE、DELETE语句修改或删除数据库中的数据 - 获取数据库元信息:通过注入查询数据库版本、表结构等信息,为后续攻击做准备
静态SQL的正确使用方法
静态SQL是指语句结构在编译阶段就已经固定,不会在运行时动态拼接用户输入的SQL,通常配合参数化查询使用,从根源上避免SQL注入风险。
使用参数化查询替代字符串拼接
参数化查询会将用户输入的内容作为参数传递给数据库,数据库会把参数部分当作纯数据处理,不会解析为SQL语法的一部分,因此可以有效防止注入。以下是使用PreparedStatement改造后的安全代码示例:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class StaticSqlDemo {
public static void main(String[] args) throws Exception {
// 模拟用户输入,即使输入恶意内容也不会触发注入
String userInput = "admin' OR '1'='1";
// 使用占位符?代替直接拼接,SQL结构固定
String sql = "SELECT * FROM user WHERE username = ?";
System.out.println("预编译的SQL语句:" + sql);
// 建立数据库连接
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "password");
// 使用PreparedStatement预编译SQL
PreparedStatement pstmt = conn.prepareStatement(sql);
// 设置参数,参数会被当作字符串处理,不会解析为SQL语法
pstmt.setString(1, userInput);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println("用户ID:" + rs.getInt("id") + ",用户名:" + rs.getString("username"));
}
rs.close();
pstmt.close();
conn.close();
}
}
静态SQL使用的注意事项
- 所有涉及用户输入的数据库查询、更新操作都使用参数化查询,不要拼接字符串
- 如果必须使用动态SQL,要对用户输入做严格的类型校验和特殊字符过滤,比如限制输入只能是数字、字母,过滤单引号、分号等危险字符
- 遵循最小权限原则,数据库账号只授予业务需要的最小操作权限,避免注入后造成更大范围的破坏
- 定期对代码做安全审计,排查动态SQL拼接的漏洞点,及时修复风险
不同场景下的SQL选择建议
如果业务场景的查询条件固定,优先使用静态SQL配合参数化查询,安全性和性能都更有保障。如果确实需要动态拼接条件,比如多条件可选查询,可以采用以下安全方式:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class SafeDynamicSqlDemo {
public static void main(String[] args) throws Exception {
// 模拟用户输入的查询条件,允许为空
String username = "admin";
Integer age = 20;
// 存储查询条件和参数
List<String> conditions = new ArrayList<>();
List<Object> params = new ArrayList<>();
// 动态拼接条件,但只用占位符,不拼接参数值
String baseSql = "SELECT * FROM user WHERE 1=1";
if (username != null && !username.isEmpty()) {
conditions.add("username = ?");
params.add(username);
}
if (age != null) {
conditions.add("age = ?");
params.add(age);
}
// 拼接完整SQL
String sql = baseSql;
for (String cond : conditions) {
sql += " AND " + cond;
}
System.out.println("生成的SQL语句:" + sql);
// 执行查询
Connection conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "password");
PreparedStatement pstmt = conn.prepareStatement(sql);
// 设置参数
for (int i = 0; i < params.size(); i++) {
pstmt.setObject(i + 1, params.get(i));
}
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
System.out.println("用户ID:" + rs.getInt("id") + ",用户名:" + rs.getString("username") + ",年龄:" + rs.getInt("age"));
}
rs.close();
pstmt.close();
conn.close();
}
}
这种方式既满足了动态条件的需求,又通过参数化查询避免了注入风险,是动态SQL场景下的安全实践方式。