目录

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'';