Java SQL注入防范
SQL注入是Web应用最常见的安全漏洞,可导致数据泄露、篡改甚至系统被控制。
SQL注入原理
正常SQL
SQL
SELECT * FROM users WHERE name = '张三' AND password = '123456'
注入攻击
Java
// 用户输入
String name = "admin' OR '1'='1";
String password = "anything";
// 拼接SQL(危险!)
String sql = "SELECT * FROM users WHERE name = '" + name +
"' AND password = '" + password + "'";
// 实际执行的SQL:
SELECT * FROM users WHERE name = 'admin' OR '1'='1' AND password = 'anything'
-- '1'='1' 永真,绕过密码验证,返回所有用户!
注入类型
| 类型 | 说明 | 示例 |
|---|---|---|
| 注入查询 | 修改查询逻辑 | ' OR '1'='1 |
| 注入删除 | 删除数据 | '; DROP TABLE users; -- |
| 注入更新 | 修改数据 | '; UPDATE users SET role='admin'; |
| 注入执行 | 执行系统命令 | xp_cmdshell(SQL Server) |
| 联合查询 | 获取其他数据 | UNION SELECT * FROM passwords |
防范方法
1. 使用PreparedStatement(首要)
Java
// 不安全:拼接SQL
String sql = "SELECT * FROM users WHERE name = '" + name + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
// 安全:PreparedStatement参数化
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name); // 参数安全设置,不会注入
ResultSet rs = pstmt.executeQuery();
PreparedStatement原理:
- SQL先预编译,结构固定
- 参数单独传递,不参与SQL解析
- 参数值不会被当作SQL执行
2. 所有输入参数化
Java
// 查询条件
PreparedStatement pstmt = conn.prepareStatement(
"SELECT * FROM users WHERE name = ? AND age > ?");
pstmt.setString(1, name);
pstmt.setInt(2, age);
// INSERT
PreparedStatement pstmt = conn.prepareStatement(
"INSERT INTO users(name, password) VALUES(?, ?)");
pstmt.setString(1, name);
pstmt.setString(2, password);
// UPDATE
PreparedStatement pstmt = conn.prepareStatement(
"UPDATE users SET name = ? WHERE id = ?");
pstmt.setString(1, newName);
pstmt.setInt(2, id);
// DELETE
PreparedStatement pstmt = conn.prepareStatement(
"DELETE FROM users WHERE id = ?");
pstmt.setInt(1, id);
3. 动态列名/表名处理
PreparedStatement不能参数化列名和表名:
Java
// 不安全:动态列名
String sql = "SELECT " + columnName + " FROM users"; // 可注入!
// 安全:白名单验证
Set<String> allowedColumns = Set.of("id", "name", "age");
if (!allowedColumns.contains(columnName)) {
throw new IllegalArgumentException("非法列名");
}
String sql = "SELECT " + columnName + " FROM users";
// 或使用枚举
public enum UserColumn { ID, NAME, AGE }
String sql = "SELECT " + userColumn.name().toLowerCase() + " FROM users";
4. 输入验证过滤
Java
// 数字验证
public int parseId(String id) {
try {
return Integer.parseInt(id);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("ID必须是数字");
}
}
// 长度限制
public void validateName(String name) {
if (name == null || name.length() > 50) {
throw new IllegalArgumentException("名称长度不合法");
}
}
// 特殊字符过滤(辅助措施)
public String sanitize(String input) {
return input.replaceAll("'", "")
.replaceAll("--", "")
.replaceAll(";", "");
}
// 但主要还是靠PreparedStatement,过滤只是辅助
5. MyBatis安全使用
XML
<!-- 不安全:${}字符串拼接 -->
<select id="findByName">
SELECT * FROM users WHERE name = '${name}' <!-- 可注入! -->
</select>
<!-- 安全:#{}参数化 -->
<select id="findByName">
SELECT * FROM users WHERE name = #{name} <!-- 安全 -->
</select>
规则:
#{}:参数化,安全,相当于PreparedStatement${}:字符串拼接,危险,只用于动态列名/表名
XML
<!-- 动态排序(需验证) -->
<select id="findAll">
SELECT * FROM users ORDER BY ${orderBy} <!-- 需白名单验证 -->
</select>
6. JPA/Hibernate安全
Java
// 安全:参数绑定
@Entity
@Table(name = "users")
public class User { ... }
// Spring Data JPA
public interface UserRepository extends JpaRepository<User, Long> {
// 安全:方法名查询
User findByName(String name);
// 安全:@Query参数化
@Query("SELECT u FROM User u WHERE u.name = :name")
User findByNameQuery(@Param("name") String name);
}
// 不安全:拼接JPQL(避免!)
String jpql = "SELECT u FROM User u WHERE u.name = '" + name + "'";
Query query = em.createQuery(jpql); // 可注入!
常见注入场景
登录注入
Java
// 攻击输入
username = "admin'--"
password = "anything"
// 原SQL
SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'
-- 注释掉密码验证,直接登录admin!
搜索注入
Java
// 攻击输入
keyword = "'; DROP TABLE products; --"
// 原SQL
SELECT * FROM products WHERE name LIKE '%'; DROP TABLE products; --%'
-- 删除products表!
排序注入
Java
// 攻击输入
orderBy = "id; DROP TABLE users"
// 原SQL
SELECT * FROM users ORDER BY id; DROP TABLE users
-- 删除users表!
安全编码规范
规范清单
| 规范 | 说明 |
|---|---|
| 使用PreparedStatement | 所有SQL参数使用参数化 |
| 禁止拼接SQL | 不要用字符串拼接SQL |
| 白名单验证 | 动态列名/表名用白名单 |
| 输入验证 | 验证输入格式和长度 |
| 最小权限 | 数据库用户只给必要权限 |
| 错误隐藏 | 不暴露SQL错误详情 |
错误处理
Java
// 不安全:暴露SQL详情
try {
stmt.executeQuery(sql);
} catch (SQLException e) {
throw new RuntimeException("SQL错误: " + e.getMessage()); // 暴露SQL!
}
// 安全:隐藏SQL详情
try {
stmt.executeQuery(sql);
} catch (SQLException e) {
throw new RuntimeException("系统错误,请联系管理员"); // 隐藏细节
// 记录日志但不返回给用户
}
检测注入漏洞
代码审查
- 检查所有SQL拼接
- 检查是否使用PreparedStatement
- 检查MyBatis的${}使用
- 检查动态列名/表名验证
渗透测试
Java
// 测试输入
1' OR '1'='1
1; DROP TABLE test
admin'--
' UNION SELECT * FROM passwords --
注意事项
PreparedStatement是防范SQL注入的首要手段
动态列名/表名需白名单验证
MyBatis用#{}不用${}
输入验证只是辅助,不能替代参数化
不要暴露SQL错误细节给用户
数据库用户权限最小化
要点总结
- SQL注入修改SQL逻辑,可绕过验证、泄露数据
- PreparedStatement参数化是防范首要手段
- 动态列名/表名需白名单验证
- MyBatis #{}安全,${}危险
- 输入验证+最小权限+隐藏错误是辅助措施
📝 发现内容有误?点击此处直接编辑