审计JAVA下的SQL注入
字数 2305 2025-08-24 23:51:18

Java应用SQL注入审计与防御指南

1. JDBC下的SQL注入

1.1 Statement与SQL注入

Statement是JDBC中执行SQL语句的基本方式,但存在严重的安全隐患:

String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);

注入示例

  • 输入:username = admin' and 1=1#password = any
  • 最终SQL:
    SELECT * FROM users WHERE username = 'admin' and 1=1#' AND password = 'any'
    
  • 由于#是MySQL注释符,实际执行:
    SELECT * FROM users WHERE username = 'admin' and 1=1
    

1.2 PreparedStatement防御机制

PreparedStatement通过预编译和参数绑定防止SQL注入:

String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

防御原理

  1. 预编译:SQL语句结构在预编译阶段确定
  2. 参数化查询:用户输入作为数据而非SQL语句部分
  3. 自动转义:特殊字符如单引号会被转义('\'

1.3 预编译的误用与风险

1.3.1 不安全拼接

// 错误示例:先拼接再预编译
String sql = "SELECT * FROM users WHERE username = '" + req.getParameter("username") 
           + "' AND password = '" + req.getParameter("password") + "'";
PreparedStatement pstmt = connection.prepareStatement(sql);

1.3.2 IN语句处理

不安全方式

String sql = "delete from users where id in("+delIds+")";

安全解决方案

public int gradeDelete(Connection con, String delIds) throws Exception {
    String[] ids = delIds.split(",");
    String placeholders = String.join(",", Collections.nCopies(ids.length, "?"));
    
    String sql = "delete from users where id in(" + placeholders + ")";
    PreparedStatement pstmt = con.prepareStatement(sql);
    
    for(int i = 0; i < ids.length; i++) {
        pstmt.setInt(i+1, Integer.parseInt(ids[i]));
    }
    return pstmt.executeUpdate();
}

1.3.3 LIKE语句处理

不安全方式

String sql = "select * from users where username like '%" + con + "%'";

安全解决方案

String sql = "select * from users where username like ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, "%" + con + "%");

1.3.4 ORDER BY限制

ORDER BY子句无法使用预编译占位符,因为字段名不能加引号:

// 无法使用预编译的安全方式
String sql = "select * from users order by ?";
// 必须使用拼接,需严格过滤
String column = "username"; // 应做白名单校验
String sql = "select * from users order by " + column;

1.4 PreparedStatement实现原理

  1. 词法分析:解析SQL语句结构
  2. 参数标记?作为占位符
  3. 输入处理
    • 对字符串类型参数自动添加引号
    • 对特殊字符进行转义
    • 数值类型直接嵌入

关键代码路径

  1. PreparedStatement.setString()StringUtils.escapeString()
  2. 对单引号等特殊字符添加转义符
  3. 最终生成的安全SQL语句结构固定

2. MyBatis下的SQL注入

2.1 #{}与${}的区别

特性 #{} ${}
处理方式 预编译参数化 直接字符串替换
安全性 安全 不安全
引号处理 自动添加 不处理
适用场景 值参数 表名、列名等非值部分

2.2 ${}导致的注入

Mapper示例

<select id="getUser" resultType="User">
    SELECT * FROM users WHERE username = '${username}'
</select>

注入攻击

  • 输入:username = admin' or '1'='1
  • 生成SQL:
    SELECT * FROM users WHERE username = 'admin' or '1'='1'
    

2.3 安全使用模式

2.3.1 LIKE语句

<!-- MySQL -->
<select id="search" resultType="User">
    SELECT * FROM users WHERE username LIKE CONCAT('%', #{keyword}, '%')
</select>

<!-- Oracle -->
<select id="search" resultType="User">
    SELECT * FROM users WHERE username LIKE '%' || #{keyword} || '%'
</select>

2.3.2 IN语句

<select id="getByIds" resultType="User">
    SELECT * FROM users WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

2.3.3 ORDER BY处理

不安全方式

ORDER BY ${columnName}

安全方案

  1. 使用Java代码校验排序字段
    // 白名单校验
    List<String> validColumns = Arrays.asList("id", "username", "create_time");
    if(!validColumns.contains(columnName)) {
        columnName = "id"; // 默认值
    }
    
  2. 在Mapper中使用条件判断
    <select id="getUsers" resultType="User">
        SELECT * FROM users
        ORDER BY 
        <choose>
            <when test="orderBy == 'name'">username</when>
            <when test="orderBy == 'time'">create_time</when>
            <otherwise>id</otherwise>
        </choose>
    </select>
    

2.4 MyBatis实现原理

2.4.1 #{}处理流程

  1. XML解析XMLScriptBuilder识别#{}模式
  2. 参数标记:将#{param}替换为?
  3. 参数绑定:通过ParameterHandler设置参数值
  4. 最终执行:调用JDBC的PreparedStatement

2.4.2 ${}处理流程

  1. 文本替换:直接替换${param}为参数值
  2. SQL拼接:生成完整SQL语句
  3. Statement执行:使用普通Statement执行

3. Hibernate下的SQL注入

3.1 HQL注入

不安全HQL

String hql = "from User where username = '" + username + "' and password = '" + password + "'";
Query<User> query = session.createQuery(hql, User.class);

安全参数绑定方式

  1. 位置参数:

    String hql = "from User where username = ?1 and password = ?2";
    Query<User> query = session.createQuery(hql, User.class);
    query.setParameter(1, username);
    query.setParameter(2, password);
    
  2. 命名参数:

    String hql = "from User where username = :name and password = :pass";
    Query<User> query = session.createQuery(hql, User.class);
    query.setParameter("name", username);
    query.setParameter("pass", password);
    

3.2 原生SQL注入

不安全方式

String sql = "select * from users where username = '" + username + "'";
Query<User> query = session.createNativeQuery(sql, User.class);

安全方式

String sql = "select * from users where username = ?";
Query<User> query = session.createNativeQuery(sql, User.class);
query.setParameter(1, username);

3.3 Hibernate实现原理

Hibernate最终仍调用JDBC的PreparedStatement实现参数化查询:

  1. 对HQL进行解析和转换
  2. 生成带占位符的SQL
  3. 通过ParameterBinder绑定参数
  4. 调用PreparedStatement.setXXX()方法

4. 防御最佳实践

  1. 优先使用预编译:始终使用PreparedStatement#{}
  2. 严格校验输入:对不可预编译部分(如ORDER BY)进行白名单校验
  3. 最小权限原则:数据库用户仅授予必要权限
  4. 防御层叠:结合输入验证、输出编码等多层防御
  5. 安全审计:定期进行代码审计和渗透测试

防御矩阵

场景 安全方案 风险等级
普通查询 预编译参数化
LIKE查询 CONCAT/函数拼接
IN查询 动态生成占位符
ORDER BY 白名单校验
表名/列名 应用层校验或映射

通过理解这些框架的SQL注入原理和防御机制,开发者可以构建更安全的Java应用程序,有效防范SQL注入攻击。

Java应用SQL注入审计与防御指南 1. JDBC下的SQL注入 1.1 Statement与SQL注入 Statement是JDBC中执行SQL语句的基本方式,但存在严重的安全隐患: 注入示例 : 输入: username = admin' and 1=1# , password = any 最终SQL: 由于 # 是MySQL注释符,实际执行: 1.2 PreparedStatement防御机制 PreparedStatement通过预编译和参数绑定防止SQL注入: 防御原理 : 预编译 :SQL语句结构在预编译阶段确定 参数化查询 :用户输入作为数据而非SQL语句部分 自动转义 :特殊字符如单引号会被转义( ' → \' ) 1.3 预编译的误用与风险 1.3.1 不安全拼接 1.3.2 IN语句处理 不安全方式 : 安全解决方案 : 1.3.3 LIKE语句处理 不安全方式 : 安全解决方案 : 1.3.4 ORDER BY限制 ORDER BY子句无法使用预编译占位符,因为字段名不能加引号: 1.4 PreparedStatement实现原理 词法分析 :解析SQL语句结构 参数标记 : ? 作为占位符 输入处理 : 对字符串类型参数自动添加引号 对特殊字符进行转义 数值类型直接嵌入 关键代码路径 : PreparedStatement.setString() → StringUtils.escapeString() 对单引号等特殊字符添加转义符 最终生成的安全SQL语句结构固定 2. MyBatis下的SQL注入 2.1 #{}与${}的区别 | 特性 | #{} | ${} | |------------|------------------------------|----------------------| | 处理方式 | 预编译参数化 | 直接字符串替换 | | 安全性 | 安全 | 不安全 | | 引号处理 | 自动添加 | 不处理 | | 适用场景 | 值参数 | 表名、列名等非值部分 | 2.2 ${}导致的注入 Mapper示例 : 注入攻击 : 输入: username = admin' or '1'='1 生成SQL: 2.3 安全使用模式 2.3.1 LIKE语句 2.3.2 IN语句 2.3.3 ORDER BY处理 不安全方式 : 安全方案 : 使用Java代码校验排序字段 在Mapper中使用条件判断 2.4 MyBatis实现原理 2.4.1 #{}处理流程 XML解析 : XMLScriptBuilder 识别 #{} 模式 参数标记 :将 #{param} 替换为 ? 参数绑定 :通过 ParameterHandler 设置参数值 最终执行 :调用JDBC的 PreparedStatement 2.4.2 ${}处理流程 文本替换 :直接替换 ${param} 为参数值 SQL拼接 :生成完整SQL语句 Statement执行 :使用普通 Statement 执行 3. Hibernate下的SQL注入 3.1 HQL注入 不安全HQL : 安全参数绑定方式 : 位置参数: 命名参数: 3.2 原生SQL注入 不安全方式 : 安全方式 : 3.3 Hibernate实现原理 Hibernate最终仍调用JDBC的 PreparedStatement 实现参数化查询: 对HQL进行解析和转换 生成带占位符的SQL 通过 ParameterBinder 绑定参数 调用 PreparedStatement.setXXX() 方法 4. 防御最佳实践 优先使用预编译 :始终使用 PreparedStatement 或 #{} 严格校验输入 :对不可预编译部分(如ORDER BY)进行白名单校验 最小权限原则 :数据库用户仅授予必要权限 防御层叠 :结合输入验证、输出编码等多层防御 安全审计 :定期进行代码审计和渗透测试 防御矩阵 : | 场景 | 安全方案 | 风险等级 | |--------------|----------------------------------|---------| | 普通查询 | 预编译参数化 | 低 | | LIKE查询 | CONCAT/函数拼接 | 中 | | IN查询 | 动态生成占位符 | 中 | | ORDER BY | 白名单校验 | 高 | | 表名/列名 | 应用层校验或映射 | 高 | 通过理解这些框架的SQL注入原理和防御机制,开发者可以构建更安全的Java应用程序,有效防范SQL注入攻击。