【代码审计】Java代码审计SQL注入

JDBC 拼接不当造成 SQL 注入

1. JDBC的两种SQL执行方法:Statement vs PrepareStatement

在JDBC中,有两种常用的执行SQL语句的方法:StatementPrepareStatement

  • Statement

    • Statement是直接拼接SQL语句执行的,每次执行时都需要对SQL语句重新进行编译,开销较大。
    • 不安全,容易受到SQL注入攻击的影响,因为用户输入的内容可以直接插入到SQL语句中并执行。
  • PrepareStatement

    • PrepareStatement会对SQL语句进行预编译,效率更高,安全性更好。
    • 支持参数化查询,使用?作为占位符,将用户输入的参数作为独立的元素传递给数据库,可以防止SQL注入。
    • 然而,如果开发者使用PrepareStatement时仍采用字符串拼接的方式构建SQL语句,则仍然存在SQL注入风险。

2. 使用Statement拼接SQL语句的示例与分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.demo.jdbcinjection;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.sql.*;

@RestController
@RequestMapping("/jdbcsql")
public class JdbcDynamicController {
private static String driver = "com.mysql.cj.jdbc.Driver";
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.username}")
private String user;
@Value("${spring.datasource.password}")
private String password;

@RequestMapping("/dynamic")
public String jdbcdynamic(@RequestParam("id") String id) throws ClassNotFoundException, SQLException{
StringBuilder result = new StringBuilder();
Class.forName(driver);
Connection conn = DriverManager.getConnection(url, user, password);
Statement statement = conn.createStatement();
String sql = "select * from user where id = '" + id + "'";
ResultSet rs = statement.executeQuery(sql);
while (rs.next()){
String rsUsername = rs.getString("username");
String rsPassword = rs.getString("password");
String info = String.format("%s: %s\n", rsUsername, rsPassword);
result.append(info);
}
rs.close();
conn.close();
return result.toString();
}
}


我在数据库里使用下面语句创建了数据库插入了一些模拟数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE DATABASE IF NOT EXISTS java;
USE java;

CREATE TABLE IF NOT EXISTS user (
id VARCHAR(255) PRIMARY KEY,
username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL
);

INSERT INTO user (id, username, password) VALUES
('1', 'user1', 'password1'),
('2', 'user2', 'password2'),
('3', 'user3', 'password3');
  • SQL注入分析
    • req.getParameter("id")获取了用户输入的参数,直接拼接到sql语句中。
    • 如果用户输入1%27%20or%201=1%23作为id参数,SQL语句将变成:
      1
      select * from user where id = '1' or 1=1 #'
    • 这个条件1=1恒为真,因此数据库将返回所有用户信息,导致信息泄露。

3. PrepareStatement的错误用法示例

即使使用了PrepareStatement,如果开发者将用户输入通过拼接方式插入到SQL语句中,仍然会出现SQL注入问题。以下代码展示了这种错误用法:

1
2
3
4
5
6
7
8
9
10
11
12
String sql = "select * from user where id = " + req.getParameter("id");
out.println(sql);
try {
PreparedStatement pstt = con.prepareStatement(sql);
ResultSet rs = pstt.executeQuery();
while (rs.next()) {
out.println("<br> id: " + rs.getObject("id"));
out.println("<br> name: " + rs.getObject("name"));
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
  • 问题分析
    • 这里虽然使用了PreparedStatement,但sql语句仍然通过拼接构造。这意味着在SQL执行前,用户的输入已经插入到SQL语句中。
    • 用户仍然可以通过输入类似1 OR 1=1的字符串来注入SQL,导致和上例相同的风险。

4. 正确使用PrepareStatement避免SQL注入

正确的方式是使用?作为占位符,并在执行SQL时将用户输入的参数绑定到占位符上。PrepareStatement会对每个参数的类型和内容进行严格检查,避免将用户输入当作SQL语句的一部分来解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PrintWriter out = resp.getWriter();
String sql = "select * from user where id = ?";
out.println(sql);
try {
PreparedStatement pstt = con.prepareStatement(sql);
pstt.setInt(1, Integer.parseInt(req.getParameter("id"))); // 使用占位符并绑定参数
ResultSet rs = pstt.executeQuery();
while (rs.next()) {
out.println("<br> id: " + rs.getObject("id"));
out.println("<br> name: " + rs.getObject("name"));
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}

MyBatis 框架代码注入

1. MyBatis 框架的简介

MyBatis 是一种将 SQL 语句嵌入到 XML 配置文件中的持久层框架,它通过将 SQL 语句和 Java 代码分离,使 SQL 语句的管理更加直观、易于修改,同时支持 SQL 参数化查询。这一特点可以在一定程度上提高 SQL 安全性,但也可能引入新的安全隐患,特别是在不正确使用 #{}${} 占位符的情况下。

2. MyBatis 中的两种 SQL 参数传递方式:#{}${}

在 MyBatis 中,开发者可以通过 #{}${} 两种方式传递参数:

  • **#{Parameter}**:使用 #{} 传参的方式会自动将参数替换为 SQL 中的占位符 ?,并在执行时进行预编译。因此,参数作为独立数据传入 SQL 执行环境,不会被解析为 SQL 的一部分。

    • 这种方式是参数化查询的安全方式,有效防止 SQL 注入。
    • 例如:
      1
      2
      3
      <select id="getUsername" resultType="com.z1ng.bean.User">
      SELECT id, name, age FROM user WHERE name = #{name}
      </select>
    • 执行过程
      • 当用户输入 name = "z1ng" 时,MyBatis 会将其转换为 SQL 查询:SELECT id, name, age FROM user WHERE name = ?
      • 在数据库执行时,z1ng 会作为参数绑定到 SQL 中的 ?,防止 SQL 注入。
  • **${Parameter}**:使用 ${} 传参的方式会直接将参数值拼接到 SQL 语句中,而不会经过预编译。这意味着参数会被解析为 SQL 语句的一部分,容易产生 SQL 注入风险。

    • 不安全的方式,不建议用于直接处理用户输入的变量。
    • 例如:
      1
      2
      3
      <select id="getUsername" resultType="com.z1ng.bean.User">
      SELECT id, name, age FROM user WHERE name = ${name}
      </select>
    • 执行过程
      • 当用户输入 name = "'aaaa' OR 1=1" 时,MyBatis 会将其直接拼接到 SQL 中,生成 SELECT id, name, age FROM user WHERE name = 'aaaa' OR 1=1
      • 由于 OR 1=1 始终为真,SQL 语句将返回所有数据,从而导致 SQL 注入。
1
2
3
4
5
6
7
8
9
10
@Mapper
public interface UserMapper {
List<User> orderbyInjection(@RequestParam("sort") String sort);
@Select("select * from user where id in (${params})")
List<User> inInjection(@Param("params")String params);
List<User> likeInjection(@Param("username") String username);
//Mybatis查询SQL语句的另一种使用注解方式,这也是存在SQL注入的。
//@Select("select * from users where username = '${username}'")
//List<User> likeInjection(@Param("username") String username);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.sqlinjection.mybatisinjection.UserMapper">
<resultMap type="com.example.sqlinjection.mybatisinjection.User" id="User">
<id column="id" property="id" javaType="java.lang.Integer" jdbcType="NUMERIC"/>
<id column="username" property="username" javaType="java.lang.String" jdbcType="VARCHAR"/>
<id column="password" property="password" javaType="java.lang.String" jdbcType="VARCHAR"/>
</resultMap>
<select id="orderbyInjection" parameterType="String" resultMap="User">
select * from user order by ${sort} asc
</select>
<select id="likeInjection" parameterType="String" resultMap="User">
select * from user where username like '%${username}%'
</select>
</mapper>

3. SQL 注入风险分析

使用 #{} 的安全性

  • 当我们在 MyBatis 中使用 #{} 传参时,MyBatis 会将参数转换为 SQL 语句的 ? 占位符,并在执行时绑定实际的参数值。这种方式确保了用户输入和 SQL 语句的分离,因此即使用户输入恶意数据,也不会对 SQL 语句的逻辑产生影响。
  • 例如,当用户输入 name = "z1ng' OR '1'='1",SQL 语句将是:
    1
    SELECT id, name, age FROM user WHERE name = ?
    在执行时,数据库会将 name 作为参数来处理,而不会改变 SQL 语句本身,从而有效防止了 SQL 注入。

使用 ${} 的 SQL 注入风险

  • 直接拼接用户输入的字符串会让 SQL 语句变得极其脆弱,用户可以通过恶意构造的输入来操纵查询逻辑。
  • 例如,在 MyBatis 中,如果使用 ${name},则输入 "z1ng' OR '1'='1" 会使 SQL 语句变为:
    1
    SELECT id, name, age FROM user WHERE name = 'z1ng' OR '1'='1'
    • SQL 语句的逻辑已被篡改,将查询所有数据,从而导致数据泄露。

6. MyBatis 使用中的安全建议

6.1 避免在动态 SQL 中直接使用 ${}

  • MyBatis 支持动态 SQL(如 <if><choose><where> 等标签),在构建动态 SQL 时也需要特别小心,避免使用 ${} 来插入用户输入。
  • 例如,在 MyBatis 的动态查询中使用 #{} 参数可以确保用户输入的数据安全。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <select id="getUserByCondition" resultType="com.example.User">
    SELECT * FROM users
    <where>
    <if test="username != null">
    AND username = #{username}
    </if>
    <if test="age != null">
    AND age = #{age}
    </if>
    </where>
    </select>

6.2 使用 sql 标签复用安全查询片段

  • MyBatis 支持将查询片段定义在 <sql> 标签中,避免开发者在多个位置拼接相似 SQL 语句,有助于规范化 SQL 查询结构,降低出错风险。
    1
    2
    3
    4
    5
    6
    7
    <sql id="userColumns">
    id, username, age, email
    </sql>

    <select id="getUserById" resultType="com.example.User">
    SELECT <include refid="userColumns"/> FROM users WHERE id = #{id}
    </select>

Hibernate 框架注入

1. Hibernate 框架概述

Hibernate 是一种持久层框架,实现了 Java 持久化 API(JPA)规范,用于在 Java 类和数据库表之间进行映射。Hibernate 允许开发者通过操作 Java 对象来与数据库交互,而无需编写原生 SQL 语句。Hibernate 主要通过 HQL 查询语言和 Criteria API 进行对象持久化,支持自动将 Java 数据类型映射到 SQL 数据类型。

Hibernate 需要一个配置文件(hibernate.cfg.xml),其中包含了数据库连接信息和一些基本的 Hibernate 设置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
<session-factory>
<property name="hibernate.dialect">org.hibernate.dialect.MySQLDialect</property>
<property name="hibernate.connection.driver_class">com.mysql.cj.jdbc.Driver</property>
<property name="hibernate.connection.url">jdbc:mysql://localhost:3306/mydb</property>
<property name="hibernate.connection.username">root</property>
<property name="hibernate.connection.password">password</property>
<property name="hibernate.hbm2ddl.auto">update</property>
<property name="hibernate.show_sql">true</property>
</session-factory>
</hibernate-configuration>

在 Hibernate 中,Java 类通常使用注解来表示实体,并将其与数据库表相关联。例如,以下是一个简单的 User 类及其数据库映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class User {
@Id // @Id 指定了主键。
@GeneratedValue(strategy = GenerationType.IDENTITY) // @GeneratedValue 配置了主键的生成策略。
private Long id;
private String username;
private String password;

// Getters and setters
}

Hibernate 提供了 Session 对象,用于管理和操作实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;

public class Main {
public static void main(String[] args) {
// 创建 SessionFactory
SessionFactory sessionFactory = new Configuration().configure().buildSessionFactory();
// 获取 Session
Session session = sessionFactory.openSession();

// 开启事务
session.beginTransaction();

// 创建用户
User user = new User();
user.setUsername("admin");
user.setPassword("12345");

// 保存用户到数据库
session.save(user);

// 提交事务
session.getTransaction().commit();

// 关闭 Session
session.close();
sessionFactory.close();
}
}

Hibernate 提供了 HQL(Hibernate Query Language)和 Criteria API 来查询数据库。

1
2
3
4
5
6
7
8
9
10
11
// HQL查询
List<User> users = session.createQuery("from User where username = :username", User.class)
.setParameter("username", "admin")
.getResultList();

// Criteria 查询
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<User> query = builder.createQuery(User.class);
Root<User> root = query.from(User.class);
query.select(root).where(builder.equal(root.get("username"), "admin"));
List<User> users = session.createQuery(query).getResultList();

2. HQL 与 SQL 注入

HQL 注入示例

以下代码展示了不安全的 HQL 查询,开发者直接将用户输入拼接到查询语句中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
factory = new Configuration().configure().buildSessionFactory();
Session session = factory.openSession();
Transaction tx = null;
try {
tx = session.beginTransaction();
String parameter = " zaaaa' or '1'='1 ";
List<User> users = session.createQuery("FROM User WHERE name = '" + parameter + "'", User.class).getResultList();
for (User user : users) {
System.out.println(user.toString());
}
tx.commit();
} catch (HibernateException e) {
if (tx != null) tx.rollback();
e.printStackTrace();
} finally {
session.close();
}
  • 注入风险分析
    • 变量 parameter 是用户输入的内容,当用户输入 "zaaaa' or '1'='1" 时,最终拼接成的 HQL 语句为:
      1
      FROM User WHERE name = 'zaaaa' OR '1'='1'
    • 因为 OR '1'='1' 恒为真,查询将返回表中所有用户的记录。
    • 此处,用户通过注入语句控制了查询的逻辑,可能导致数据泄露。

3. 预防 HQL 注入的参数绑定方式

在 Hibernate 中,安全的参数绑定方式包括位置参数、命名参数、命名参数列表和类实例。这些方法均采用预编译,确保参数化查询,有效防止 SQL 注入。

3.1 位置参数(Positional Parameter)

位置参数通过 ? 占位符指定参数位置,然后使用 setParameter 方法绑定参数值。

1
2
3
String parameter = "z1ng";
Query<User> query = session.createQuery("FROM User WHERE name = ?1", User.class);
query.setParameter(1, parameter);
  • 执行过程
    • ?1 表示第一个参数位置,在查询时将自动绑定 parameter 的值。
    • 位置参数的预编译可以确保输入被当作数据处理,而不是查询语句的一部分,从而避免 SQL 注入。

3.2 命名参数(Named Parameter)

命名参数通过 :parameterName 格式指定参数名,并使用 setParameter 方法绑定具体值。相比位置参数,命名参数更具可读性。

1
2
3
String parameter = "z1ng";
Query<User> query = session.createQuery("FROM User WHERE name = :name", User.class);
query.setParameter("name", parameter);
  • 执行过程
    • :name 为命名参数,占位符被替换为 parameter 的值。
    • Hibernate 会将 parameter 作为数据绑定到查询语句,不会影响查询逻辑。

3.3 命名参数列表(Named Parameter List)

命名参数列表允许绑定集合或数组作为查询参数,适用于 IN 查询等多值场景。

1
2
3
List<String> names = Arrays.asList("z1ng", "z2ng");
Query<User> query = session.createQuery("FROM User WHERE name IN (:names)", User.class);
query.setParameter("names", names);
  • 执行过程
    • Hibernate 会将 names 列表中每个元素作为独立参数绑定到查询,防止任何单一值干扰查询结构。

3.4 类实例(JavaBean)

通过类实例绑定参数,将对象的属性直接映射到查询参数中。可以使用 setProperties 方法批量绑定对象属性,避免逐个指定参数值。

1
2
3
user1.setName("z1ng");
Query<User> query = session.createQuery("FROM User WHERE name = :name", User.class);
query.setProperties(user1);
  • 执行过程
    • 该方法会根据 user1 对象的属性自动绑定查询参数,确保查询安全。

4. 使用原生 SQL 的注意事项

Hibernate 还支持直接执行原生 SQL 语句。当使用原生 SQL 时,拼接用户输入会导致和 JDBC 中相同的 SQL 注入风险。建议采用参数绑定方式执行原生 SQL 查询。

不安全的原生 SQL 拼接示例

1
2
String parameter = "z1ng";
Query<User> query = session.createNativeQuery("SELECT * FROM user WHERE name = '" + parameter + "'");
  • 注入风险
    • 直接将 parameter 拼接到 SQL 语句中,恶意用户可以通过输入 z1ng' OR '1'='1 改变查询逻辑。

安全的原生 SQL 参数绑定

通过参数绑定,可以避免拼接用户输入带来的注入风险:

1
2
3
String parameter = "z1ng";
Query<User> query = session.createNativeQuery("SELECT * FROM user WHERE name = :name", User.class);
query.setParameter("name", parameter);
  • 执行过程
    • 使用 :name 参数占位符,parameter 值将作为独立数据绑定,确保查询逻辑安全。

5. 其他安全建议

5.1 使用 Criteria API

Hibernate 提供的 Criteria API 可以动态构建查询,避免手动拼接 SQL 或 HQL 语句,进而降低 SQL 注入的可能性。

1
2
3
4
5
CriteriaBuilder cb = session.getCriteriaBuilder();
CriteriaQuery<User> cq = cb.createQuery(User.class);
Root<User> root = cq.from(User.class);
cq.select(root).where(cb.equal(root.get("name"), parameter));
List<User> results = session.createQuery(cq).getResultList();