目录

SQL注入 - Java栈SQL框架风险

一、常用数据库开发体系

1、框架

  • ORM(Object-Relational Mapping)框架是一种用于将对象模型和关系型数据库之间的数据映射的工具。它可以自动跟踪对象的变化,将对象的状态同步到数据库中,并自动执行CRUD操作(Create、Read、Update、Delete)。
名称 ORM框架? 说明
Spring JDBC 否 Spring框架提供的JDBC抽象层,简化了数据库操作
Spring Data JPA 是 减少为各种持久层存储实现数据访问层所需的代码量
Hibernate 是 提供了对象关系映射(ORM)框架,简化了数据库操作
Mybatis 是 一个优秀的持久层框架,支持定制化 SQL、存储过程和高级映射。
JOOQ 是 基于 SQL 的数据库映射框架,将数据库表映射为 Java 对象,同时提供了类型安全的 SQL 构建器

2、数据库

名称 关系型? 说明
MySQL 是 被广泛使用于Web应用开发中,由Oracle公司管理。
ClickHouse 是 ClickHouse是一个用于数据分析的列式数据库系统,具有高性能和高可扩展性,且支持实时查询。它是由俄罗斯的Yandex公司开发和维护,支持SQL语言。
MariaDB 是 MariaDB是一个由MySQL的叉子演化而来的关系型数据库管理系统(RDBMS),它大部分代码和架构都来自MySQL,兼容MySQL,同时也提供了一些MySQL没有的特色功能,并具有高可扩展性、高性能和高安全性等特点。
Hive 是 Hive是由Apache Hadoop创建的数据仓库工具,使用类似SQL的HiveQL查询语言用于查询、分析和管理大数据。Hive基于Hadoop Distributed File System(HDFS),它使用HiveQL构建数据仓库并将SQL查询转换为MapReduce任务运行。
PostgreSQL 是 具有高度的可靠性、可扩展性和数据完整性。
MongoDB 否 一种基于分布式文件存储的NoSQL数据库
Redis 否 一种高性能的内存数据结构存储,被广泛用于缓存和应用程序中间层。
Elasticsearch 否 一种分布式的全文搜索服务,能够快速地对大规模的数据集进行搜索和分析,并提供丰富的查询功能、聚合分析等特点。

3、连接池

名称 说明
HikariCP HikariCP是一个轻量级的高性能Java数据库连接池,尤其适合高负载的应用场景,比其他连接池更快,并提供更好的性能,支持JDBC 3和JDBC 4标准,并提供非常容易的配置。
Druid Druid是Alibaba开源的一个高性能的数据库连接池,支持JDBC 4.1规范,具有监控、防御SQL注入等特性,容易集成Spring等框架。

二、数据库框架调用风险

1、JDBC

JDBC(Java DataBase Connectivity)Java连接数据库操作的原生接口。可以用相同的操作方法去操作不同产商的DB。

/posts/sql-injection-code/out.png
img

调用方法 接口 描述 预编译
JDBC API Statement 简单的 SQL 语句执行方式,支持变量替换,安全性比较低 无
PreparedStatement 可以提前进行 SQL 编译,支持占位符传参,具有更好的可读性和安全性。 ?占位符
Spring JDBC JdbcTemplate 基于 JDBC 原生 API 进行封装,提供了更方便的 API,具有更好的可读性和安全性。 ?占位符
NamedParameterJdbcTemplate 在JdbcTemplate 的基础上,提供了通过命名参数的方式为占位符提供实际参数值的功能,从而使得 SQL 更加具有可读性和维护性。 命名参数:
SimpleJdbcTemplate 对于 JdbcTemplate 和 NamedParameterJdbcTemplate 进行了进一步的简化和封装,使用更方便,但它在 Spring 3.x 后不再建议使用,已被废弃。 ?占位符、命名参数:

JDBC API和Spring Jdbc调用不同之处

方式 Spring JDBC JDBC API
配置方式不同 数据库连接池的配置是通过配置文件来实现 显示加载并注册驱动程序,还需要手动指定与数据库的连接参数,这样代码耦合度较高。
连接池实现不同 Spring Boot内置的是HikariCP,也可以使用其他连接池实现 编写自己的连接池实现
处理异常方式不同 采用声明式事务处理的方式,即将相关的DAO方法用事务注解进行标记,当代码执行出错时,事务会自动回滚,不需要手动编写一些繁琐的JDBC异常处理代码 手动编写大量的异常处理代码,包括获取数据库连接异常、SQL语句执行异常等等,这样代码的可读性和可维护性都不够高。
调用方式不同 通过DAO层来调用SQLJdbcOperations、NamedParameterJdbcOperations、Spring Data JPA 手动编写 SQL 语句,并将 SQL 语句和 Java 数据对象进行映射,需要编写大量的转换和操作代码,从而导致代码的复杂和容易出错。

Spring Boot应用

/posts/sql-injection-code/out-20240528022003449.png
img

纯JDBC

 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
package com.txw.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

@SuppressWarnings("all")   // 注解警告信息public class JdbcTest01 {
    public static void main(String[] args) throws Exception {
        // 1.加载驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 2 创建和数据库之间的连接
        String username = "testdb";
        String password = "testxxxxxx";
        String url = "jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai";
        Connection conn = DriverManager.getConnection(url,username,password);
        // 3.准备发送SQL
        String sql = "select * from t_person";
        PreparedStatement pstm = conn.prepareStatement(sql);
        // 4.执行SQL,接收结果集
        ResultSet rs = pstm.executeQuery();
        // 5 处理结果集
        while(rs.next()){
            int personId1 = rs.getInt("person_id");
            String personName1 = rs.getString("person_name");
            int age1 = rs.getInt("age");
            String sex1 = rs.getString("sex");
            String mobile1 = rs.getString("mobile");
            String address1 = rs.getString("address");
            System.out.println("personId="+personId1+",personName="+personName1+",age="+age1+",sex="+sex1+",mobile="+mobile1+",address="+address1);
        }
        // 6.释放资源
        rs.close();
        pstm.close();
        conn.close();
    }}

(1)Statement

每次执行时都需要编译,增大系统开销

1
2
3
4
5
6
7
//注入
String sql = "select * from user where username = '" + username + "'";
Statement st = con.createStatement();
ResultSet rs = st.executeQuery(sql);

//预编译
//无预编译,使用PreparedStatement代替Statement

(2)PrepareStatement

会对 SQL 语句进行预编译,减小系统开销

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//注入
String sql = "select * from user where username = '" + username + "'";
PreparedStatement ps = con.prepareStatement(sql);
ResultSet rs = ps.executeQuery();

//预编译
String sql = "select * from user where username = ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1 ,username);
ResultSet rs = ps.executeQuery();

(3)JdbcTemplate

参数传递方式基于顺序,即通过索引来传递参数

 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
//注入
@RestController
public class JdbcController {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @GetMapping("/users")
    public List<Map<String,Object>> getUsers(String name) {
        String sql = "SELECT * FROM user WHERE username = '"+name+"'";
        List<Map<String,Object>> userList= jdbcTemplate.queryForList(sql);
        return userList;
    }
}

//预编译
@RestController
public class JdbcController {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @GetMapping("/users")
    public List<Map<String,Object>> getUsers(String name) {
        String sql = "SELECT * FROM user WHERE username = ?";
      	Object[] obj = new Object[] {name};
        List<Map<String,Object>> userList= jdbcTemplate.queryForList(sql, obj);
        return userList;
    }
}

使用List最后转成Array

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@RestController
public class JdbcController {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @GetMapping("/users")
    public List<Map<String,Object>> getUsers(String name) {
        String sql = "SELECT * FROM user WHERE username = ?";
        List<Object> params = new ArrayList<Object>();
        params.add(name);
        List<Map<String,Object>> userList= jdbcTemplate.queryForList(sql, params.toArray());
        return userList;
    }
}

(4)NamedParameterJdbcTemplate

参数传递方式基于名称,即通过参数名来传递参数

 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
//注入
@RestController
public class UserController {
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    @GetMapping("/test")
    public List<String> getTest(@RequestParam String id) {
        String sql = "SELECT * FROM user WHERE id = '"+id+"'";
        List<String> users = namedParameterJdbcTemplate.query(sql, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getInt("id")+rs.getString("username");
            }
        });
        return users;
    }
}

//预编译
@RestController
public class UserController {
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    @GetMapping("/test")
    public List<String> getTest(@RequestParam String id) {
        String sql = "SELECT * FROM user WHERE id = :id";
        Map<String, Object> params = new HashMap<>();
        params.put("id", id);
        List<String> users = namedParameterJdbcTemplate.query(sql, params, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getInt("id")+rs.getString("username");
            }
        });
        return users;
    }
}

使用javaBean

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class UserController {
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;

    @GetMapping("/test")
    public List<String> getTest(@RequestParam Long id) {
        String sql = "SELECT * FROM user u WHERE u.id = :id";
        ParamBean bean = new ParamBean();
        bean.setId(id);
        SqlParameterSource param = new BeanPropertySqlParameterSource(bean);
        List<String> users = namedParameterJdbcTemplate.query(sql, param, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                return rs.getInt("id")+rs.getString("username");
            }
        });
        return users;
    }
}

2、Hibernate

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private Integer age;

    // 省略 getter 和 setter 方法

}

entityManager查询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
@Repository
public class UserDao {

    @Autowired
    private EntityManager entityManager;

    public User findById(Long id) {
        String hql = "from User where id = :id";
        TypedQuery<User> query = entityManager.createQuery(hql, User.class);
        query.setParameter("id", id);
        return query.getSingleResult();
    }

    public List<User> findAll() {
        String hql = "from User";
        TypedQuery<User> query = entityManager.createQuery(hql, User.class);
        return query.getResultList();
    }

   public Page<User> findPage(int pageNo, int pageSize, String sortField, boolean ascending) {
        String hql = "select u from User u order by u." + sortField + (ascending ? " asc" : " desc");
        TypedQuery<User> query = entityManager.createQuery(hql, User.class);
        query.setFirstResult((pageNo - 1) * pageSize);
        query.setMaxResults(pageSize);

        List<User> resultList = query.getResultList();
        long totalCount = count();
        return new PageImpl<>(resultList, PageRequest.of(pageNo - 1, pageSize), totalCount);
    }

    public long count() {
        String hql = "select count(*) from User";
        TypedQuery<Long> query = entityManager.createQuery(hql, Long.class);
        return query.getSingleResult();
    }

}

controller

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserDao userDao;

    @GetMapping("/{id}")
    public User findById(@PathVariable Long id) {
        return userDao.findById(id);
    }

    @GetMapping
    public List<User> findAll() {
        return userDao.findAll();
    }

    @GetMapping("/page")
    public Page<User> findPage(@RequestParam int pageNo, @RequestParam int pageSize, @RequestParam String sortField, @RequestParam boolean ascending) {
        return userDao.findPage(pageNo, pageSize, sortField, ascending);
    }

}

(1)createQuery

createQuery是HQL&JPQL查询方法。HQL注入在漏洞利用上有一定的限制,比如不能利用联合查询、不能跨库查表、执行命令等。

1
2
3
4
5
6
7
8
@Autowired
private EntityManager entityManager;

public User findById(String id) {
  String hql = "from User where id = " + id; //User表名
  TypedQuery<User> query = entityManager.createQuery(hql, User.class);
  return query.getSingleResult();
}

(2)createNativeQuery

是SQL查询方法

1
2
3
4
5
6
7
8
@Autowired
private EntityManager entityManager;

public Object findById(String id) {
  String hql = "select * from user where id = " + id; //User表名
  Query query = entityManager.createNativeQuery(hql, User.class);
  return query.getSingleResult();
}

3、Spring Data JPA

JPA 即Java Persistence API的缩写,也即JAVA持久化层API,涵盖几个方面:

  • 一套标准API:在javax.persistence的包下面提供,用来操作实体对象,执行CRUD操作,将开发者从烦琐的JDBC和SQL代码中解脱出来,按照JAVA思路去编写代码操作DB。
  • 面向对象操作语言:通过面向对象的思路,避免代码与SQL的深度耦合。
  • ORM元数据映射:ORM,即Object Relation Mapping,对象关系映射。

Spring Data JPA整体实现逻辑

Spring Data JPA不是一个完整JPA规范的实现,它只是一个代码抽象层,主要用于减少为各种持久层存储实现数据访问层所需的代码量。其底层依旧是Hibernate。可以把Spring Data JPA理解为JPA规范的再次封装抽象。JAVA业务层调用SpringData JPA二次封装提供的Repository层接口,进而基于JPA标准API进行处理,基于Hibernate提供的JPA具体实现,接着基于JDBC标准API接口,完成与实际DB之间的请求交互

/posts/sql-injection-code/out-20240528022003896.png
img

名称 说明
JPA(Java Persistence API) JPA是规范,它指明了持久化、读取和管理 Java 对象映射到数据库表时的规范。 JPQL(Java Persistence Query Language)
Spring Data JPA 是 Spring Framework 的一部分。它不是 JPA 的实现,而是在 JPA 之上提供更高层次的抽象,可以减少很多模板代码。而 Spring Data JAP 的默认实现是 Hibernate,当然也可以其他的 JPA Provider。 SQL、JPQL、HQL
Hibernate Hibernate 是一个 ORM 框架,它实现了 Java 对象到数据库表的映射。也就是说,Hibernate 提供了 JPA 的一种实现。 HQL(Hibernate Query Language)

MyBatis与JPA的差异点:

  • 设计哲学不同,MyBatis偏向于面向过程,JPA则将面向对象发挥到极致;
  • MyBatis定制起来更加灵活,支持高度定制化的sql语句,支持任意编写sql语句;JPA相对更注重对已有高频简单操作场景的封装,简化开发人员的重复操作,虽然JPA也支持定制SQL语句,但是相比MyBatis灵活度略差。

添加pom

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    <version>2.5.2</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
</dependency>

入口注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@SpringBootApplication
// 可选,指定扫描的表映射实体Entity的目录,如果不指定,会扫描全部目录
//@EntityScan("com.veezean.demo.entity")
// 可选,指定扫描的表repository目录,如果不指定,会扫描全部目录
//@EnableJpaRepositories(basePackages = {"com.veezean.demo.repository"})
// 可选,开启JPA auditing能力,可以自动赋值一些字段,比如创建时间、最后一次修改时间等等
//@EnableJpaAuditing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

配置数据源,application.properties或application.yml中配置数据源:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 数据库URL、用户名、密码等信息
spring.datasource.url=jdbc:mysql://localhost:3306/javawebtest?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=sss123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#Hibernate相关的属性
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.show-sql=true
#Java代码实体字段命名与数据库表结构字段之间的名称映射策略
#spring.jpa.hibernate.naming.implicit-strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
#下面配置开启后,会禁止将驼峰转为下划线
#spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
#spring.jpa.open-in-view=false
#spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
# 控制是否可以基于程序中Entity的定义自动创建或者修改DB中表结构
#spring.jpa.properties.hibernate.hbm2ddl.auto=update

创建要持久化到数据库中的实体类,User类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Entity
@Table(name = "user")
public class User {
    @Id // 唯一标识符
    @GeneratedValue(strategy = GenerationType.IDENTITY) // 使用自增长字段生成
    private Long id;

    @Column(nullable = false, unique = true)  // 不能为空,且唯一
    private String username;

    @Column(nullable = false)
    private String password;

    // getters/setters
}

创建JpaRepository来管理实体类,UserRepository接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public interface UserRepository extends JpaRepository<User, Long> {
    User findByUsername(String username); // JPA根据方法名自动生成查询语句 /org/springframework/aop/framework/JdkDynamicAopProxy.class
    
    //原生SQL
    //@Query(value = "select * from user where username = ?1", nativeQuery = true)
    //User fuzzyQueryByName(String username);

    //HQL提供的查询语法
    //@Query("FROM User u WHERE u.username = :username")
    //User fuzzyQueryByName(@Param("username") String username);

    //JPQL1提供的查询语法
    @Query("SELECT u FROM User u WHERE u.username = :username")
    User fuzzyQueryByName(@Param("username") String username);

    //JPQL2提供的查询语法
    //@Query("SELECT u FROM User u WHERE u.username = ?1")
    //User fuzzyQueryByName(String username);
}

创建UserService服务类,使UserRepository仓库类进行数据库操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Service // 被@Autowired注解进行注入使用
@Transactional // 事务操作。如果出现了异常,事务将被回滚到之前的状态。(如果出现了异常,事务将回滚到之前的状态,保证数据的一致性和完整性。)
public class UserService {
    @Autowired // @Autowired注解寻找同名@Service 注入使用
    private UserRepository userRepository;

    public User findByUsername(String username) {
        return userRepository.findByUsername(username);
    }

    public User save(User user) {
        return userRepository.save(user);
    }

    public void deleteById(Long id) {
        userRepository.deleteById(id);
    }
  
    public User fuzzyQueryByName(String userName){
      	return userRepository.fuzzyQueryByName(userName);
}

创建控制器接口类UserController,并注入UserService服务类

 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
@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService; //初始化数据库操作服务类

    @PostMapping("/register")
    public ResponseEntity<String> register(@RequestParam String username,
            @RequestParam String password) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        userService.save(user);
        return ResponseEntity.ok("Registered.");
    }

    @GetMapping("/{username}/profile")
    public ResponseEntity<User> profile(@PathVariable String username) {
        User user = userService.findByUsername(username);
        if (user != null) {
            return ResponseEntity.ok(user);
        }
        return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
    }
  
    @GetMapping("/name")
    public User fuzzyQueryByName(@RequestParam String username) {
        return userService.fuzzyQueryByName(username)
    }
}

(1)JpaRepository

JpaRepository 是 Spring Data JPA 提供的一个接口,它是对 EntityManager 的封装,并提供了一些额外的功能,比如自动生成常见的 CRUD 操作,以及通过方法名来定义查询等。JpaRepository 还支持分页、排序、锁定等高级功能,使得对数据库的访问更加方便。

1
2
3
//编写符合规范,不存在注入问题
@Query(value = "SELECT * FROM user WHERE username = 'exampleuser' ORDER BY :orderby", nativeQuery = true)
User fuzzyQueryByName(@Param("orderby") String orderby);

4、Mybatis

pom

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
<!-- MyBatis依赖 -->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.1.4</version>
</dependency>

<!-- 数据库驱动依赖 -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.22</version>
</dependency>

User.java

 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
public class User {

    private Long id;
    private String username;
    private String password;
    
    public User() {
    }

    public User(String username, Long id, String password) {
        this.username = username;
        this.id = id;
        this.password = password;
    }
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

interface

1
2
3
4
5
6
7
8
9
@Mapper
public interface UserMapper {

    @Select("SELECT id, username, password FROM user WHERE id = #{id}")
    User findById(String id);

    @Select("SELECT id, username, password FROM user")
    List<User> findAll();
}

controller

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@RestController
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @PostMapping("/users/id")
    public User findById(String id) {
        return userMapper.findById(id);
    }

    @GetMapping("/users")
    public List<User> findAll() {
        return userMapper.findAll();
    }
}

(1)使用注解方式

可以使用@Select、@Insert、@Update、@Delete等注解来执行对应的SQL语句

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Mapper
public interface UserMapper {
  
		// 不存在注入
    @Select("SELECT id, username, password FROM user WHERE id = #{id}")
    User findById(String id);
		
  	// 存在注入
    @Select("SELECT id, username, password FROM user WHERE id = '${id}'")
    User findById(String id);
}

(2)使用Mapper XML配置文件

将SQL语句放入指定的Mapper XML配置文件中

1
2
3
4
5
6
7
8
9
# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/javawebtest?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=sss123
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

# MyBatis配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.mybatis.entity
1
2
3
4
@Mapper
public interface UserMapper {
    User findById(String id);
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mybatis.entity.UserMapper">
    <!--    sql注入-->
    <select id="findById" resultType="com.example.mybatis.User">
        SELECT * FROM user WHERE id = ${id}
    </select>
  
    <!--    预编译-->
    <select id="findById" resultType="com.example.mybatis.User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>

5、Mybatis-plus

Mybatis-Plus 是在 Mybatis 的基础上进行的封装和拓展,提供了更加方便的数据库操作方式和更好的性能优化

(1)BaseMapper

符合标准,不存在注入

1
2
public interface UserMapper extends BaseMapper<User> {
}
1
2
3
4
5
6
7
8
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    public User selectById(Long id) {
        return userMapper.selectById(id);
    }
}

(2)Wrapper 条件构造器

条件构造器Wrapper可以用于复杂的数据库操作:大于、小于、模糊查询等等。

 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
// 预编译
@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;

    @RequestMapping("/users/id")
    public List<User> selectByName(Long id) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("id", id);
        return userMapper.selectList(queryWrapper);
    }
}
@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;

    @RequestMapping("/users/id")
    public List<User> selectByName(Long id) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.apply("id={0}",id);  //用了{}防止注入
        return userMapper.selectList(queryWrapper);
    }
}

// 注入
@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;

    @RequestMapping("/users/id")
    public List<User> selectByName(Long id) {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.apply("id="+id); //用了 apply/last/exists/notExists/having/inSql/notInSql/orderBy/orderByAsc/orderByDesc/groupBy 都会注入
        return userMapper.selectList(queryWrapper);
    }
}

(3)XML

pom

1
2
3
4
5
6
<!-- Mybatis-plus -->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.3.0</version>
</dependency>

配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/javawebtest?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=sss123
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

# MyBatis配置
mybatis-plus.type-aliases-package=com.example.mybatis.entity
mybatis-plus.mapper-locations=mapper/**.xml
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

User

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Data // get/set lombok实现
@NoArgsConstructor // 无参构造器
@AllArgsConstructor // 含参垢早起
@TableName("user")
public class User {
    @TableId
    @TableField("id")
    private Long id;
    @TableField("username")
    private String userName;
    @TableField("password")
    private String password;
}

Mapper

1
2
3
public interface UserMapper extends BaseMapper<User> {
    List<User> selectNameById(Long id);
}

mapper xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<?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.mybatis.mapper.UserMapper">
    <select id="selectNameById" resultType="com.example.mybatis.entity.User">
        select * from user where id = ${id}
    </select>
    
    <!--预编译-->
    <select id="selectNameById" resultType="com.example.mybatis.entity.User">
        select * from user where id = #{id}
    </select>
  
</mapper>

controller

1
2
3
4
5
6
7
8
9
@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;
    @RequestMapping("/users/id")
    public List<User> getId(Long id){
        return userMapper.selectNameById(id);
    }
}

(4)注解(同mybatis,忽略)

忽略

6、易出现的点

  • PreparedStatement只有在使用"?“作为占位符才能预防sql注入,直接拼接仍会存在sql注入漏洞

  • 删除语句中可能会存在此类语句,由于无法确定ids含有对象个数而直接拼接sql语句,造成sql注入。

1
String sql = "delete from users where id in("+delIds+"); //存在sql注入

解决方法为遍历传入的 对象个数,使用“?”占位符。

  • 模糊查询使用like语句直接拼接会造成sql注入, 在这种情况下使用#{}程序会报错,新手程序员就把#号改成了$,这样如果java代码层面没有对用户输入的内容做处理势必会产生SQL注入漏洞。如果黑名单要过滤%和_符号,否则造成慢查询
1
String sql = "select * from users where password like '%" + con + "%'"; //存在sql注入
  • 字段名表名不能预编译,因为不能带引号

三、注入点

1、select

语法顺序 执行顺序
SELECT 7 字段名 查询的表中必须含有字段名,负责报错
FROM 1 表名 正常注入
JOIN 3 表名
ON 2 字段名
WHERE 4 字段 正常注入,如果不加GROUP BY直接写HAVING 1=1会爆表名
GROUP BY 5 字段名 报错注入,group by 后面的字段是虚拟表的主键。也就是说它是不能重复的。
HAVING 6 字段名 HAVING 语句通常与GROUP BY语句联合使用,用来过滤由GROUP BY语句返回的记录集,如group by 查找字段为 user_id 的所有数据,然后用 having 筛选 user_id=1 的那条数据。
UNION 8
ORDER BY 9 字段名 order by后面可以跟if(),case when else这样的复合查询语句。可以用来进行bool注入,延时注入等
LIMIT 10 数值型 limit后面不能直接跟select语句和if语句。可以跟procedure语句,值得注意的是只有在5.0.0< MySQL <5.6.6版本才可以使用,procedure后面支持报错注入以及时间盲注
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
SELECT
  _time AS time_col,
  sum(page) AS metric_value,
  concat('l_', type) as tag_name
FROM
  ks_abtest.result
WHERE
  _time >= '2024-04-12 00:00:00'
  AND world_name = 'holdout_kuaishou_all_did_0'
  AND type in ('r1', 'r2', 'r3')
  AND type like '%r%'
GROUP BY
  time_col, tag_name
HAVING 
  metric_value > 100
ORDER BY
  time_col ASC
LIMIT #{limit}
UNION ALL
SELECT
  '1' as tag_name
FROM
  user;

join

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
SELECT 
  r._time AS time_col,
  SUM(r.page) AS metric_value,
  CONCAT('l_', r.type) AS tag_name,
  u.username AS username
FROM 
  ks_abtest.result AS r 
JOIN 
  user AS u 
  ON r.user_id = u.user_id
WHERE 
  r._time >= '2024-04-12 00:00:00' 
  AND r.world_name = 'holdout_kuaishou_all_did_0' 
  AND r.type IN ('r1', 'r2', 'r3')
GROUP BY 
  time_col, tag_name, username
ORDER BY 
  time_col ASC 
LIMIT #{limit};

2、insert

1
2
INSERT INTO Websites (name, url, alexa, country)
VALUES ('百度','https://www.baidu.com/','4','CN');

payload:

1
INSERT INTO users (id, username, password) VALUES (2,'Olivia' or updatexml(1,concat(0x7e,(version())),0) or'', 'Nervo');

3、update

1
2
3
4
UPDATE Websites 
SET alexa='5000', country='USA' 
WHERE name='菜鸟教程' AND c.country IN ('China', 'USA') ORDER BY a desc 
LIMIT 100;

payload:

1
UPDATE users SET password='Nicky' or updatexml(2,concat(0x7e,(version())),0) or''WHERE id=2 and username='Olivia';

4、delete

1
2
3
DELETE FROM Websites
WHERE name='Facebook' AND c.country IN ('China', 'USA')  ORDER BY a desc 
LIMIT 100;

payload:

1
DELETE FROM users WHERE id=2 or updatexml(1,concat(0x7e,(version())),0) or'';