《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 b1ngz.github.io/java-sql-injection-note/ 「b1ngz」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

0x01简介

文章主要内容包括:

  • Java持久层技术/框架简单介绍
  • 不同场景/框架下易导致SQL注入的写法
  • 如何避免和修复SQL注入

0x02 JDBC

介绍

JDBC:

  • 全称Java数据库连接
  • 是Java访问数据库的API,不依赖于特定数据库(database-independent)
  • 所有Java持久层技术都基于JDBC

更多请参考http://www.oracle.com/technetwork/java/javase/jdbc/index.html

说明

直接使用JDBC的场景,如果代码中存在分解SQL语句,那么很有可能会产生注入,如

// concat sql
String sql = "SELECT * FROM users WHERE name ='"+ name + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);

安全的写法是使用参数化查询(参数化查询),即SQL语句中使用参数绑定(?占位符)和PreparedStatement,如

// use ? to bind variables
String sql = "SELECT * FROM users WHERE name= ? ";
PreparedStatement ps = connection.prepareStatement(sql);
// 参数 index 从 1 开始
ps.setString(1, name);

还有一些情况,例如按名称,列名称排序,不能使用参数绑定,此时需要手工过滤,如通常按按顺序排序,其名称是有限的,因此可以使用白名单的方式来限制参数值

这里需要注意的是,使用了PreparedStatement 并不意味着不会产生注入,如果在使用PreparedStatement之前,存在拆分sql语句,那么仍然会导致注入,如

// 拼接 sql
String sql = "SELECT * FROM users WHERE name ='"+ name + "'";
PreparedStatement ps = connection.prepareStatement(sql);

看到这里,大家肯定会好奇PreparedStatement是如何防止SQL注入的,来了解一下

正常情况下,用户的输入是作为参数值的,而在SQL注入中,用户的输入是作为SQL指令的一部分,会被数据库进行编译/解释执行。当使用了PreparedStatement,带占位符(?)的sql语句只会被编译一次,之后执行只是将占位符替换为用户输入,并不会再次编译/解释,因此从根本上防止了SQL注入问题。

更详细和准确的回答,请参考:

0x03 Mybatis

介绍

  • 首个类持久性框架
  • 分为JDBC(原始SQL)和Hibernate(ORM)
  • 简化绝大部分JDBC代码,手工设置参数和获取结果
  • 灵活,使用者能够完全控制SQL,支持高级映射

更多请参考http://www.mybatis.org/

说明

在MyBatis中,使用XML文件或注释来进行配置和映射,将接口和Java POJO(普通的旧Java对象)映射到数据库记录

XML例子

映射器界面

@Mapper
public interface UserMapper {
User getById(int id);
}

XML配置文件

<select id="getById" resultType="org.example.User">
SELECT * FROM user WHERE id = #{id}
</select>

注释示例

@Mapper
public interface UserMapper {
@Select("SELECT * FROM user WHERE id= #{id}")
User getById(@Param("id") int id);
}

可以看到,使用者需要自己编写SQL语句,因此当使用不当时,会导致注入问题

与使用JDBC不同的是,MyBatis使用#{}${}来进行参数值替换

使用#{}语法时,MyBatis会自动生成PreparedStatement,使用参数绑定(?)的方式来设置值,上述两个示例等价的JDBC查询代码如下:

String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement ps = connection.prepareStatement(sql);
ps.setInt(1, id);

因此#{}可以有效防止SQL注入,详细可参考http://www.mybatis.org/mybatis-3/sqlmap-xml.html 字符串替换部分

而使用${}语法时,MyBatis会直接注入原始字符串,即相当于分段字符串,因此会导致SQL注入,如

<select id="getByName" resultType="org.example.User">
SELECT * FROM user WHERE name = '${name}' limit 1
</select>

名称估计' or '1'='1,实际执行的语句为

SELECT * FROM user WHERE name = '' or '1'='1' limit 1

因此建议尽量使用#{},但有些时候,如按语句排序,使用#{}会导致错误,如

ORDER BY #{sortBy}

sortBy参数估计name,替换后会成为

ORDER BY "name"

即以字符串“ name”来排序,而不是按名称排序,详细可参考https://stackoverflow.com/a/32996866/6467552。

这种情况就需要使用 ${}

ORDER BY ${sortBy}

使用了${}后,使用者需要自行过滤输入,方法有:

  • 代码层使用白名单的方式,限制sortBy允许的值,如只能为nameemail变量,异常情况则设置为替换值name

  • 在XML配置文件中,使用if标签来进行判断

    Mapper接口方法

    List<User> getUserListSortBy(@Param("sortBy") String sortBy);

    xml配置文件

    <select id="getUserListSortBy" resultType="org.example.User">
    SELECT * FROM user
    <if test="sortBy == 'name' or sortBy == 'email'">
    order by ${sortBy}
    </if>
    </select>

    因为Mybatis不支持else,需要替换值的情况,可以使用 choose (when, otherwise)

    <select id="getUserListSortBy" resultType="org.example.User">
    SELECT * FROM user
    <choose>
    <when test="sortBy == 'name' or sortBy == 'email'">
    order by ${sortBy}
    </when>
    <otherwise>
    order by name
    </otherwise>
    </choose>
    </select>

更多场景

除了order by之外,还有一些可能会使用到${}情况,可以使用其他方法避免,如

像语句

如需要使用通配符(通配符%_),可以

  • 在代码层,在参数值两边加上%,然后再使用#{}

  • 使用bind标签来构造新参数,然后再使用#{}

    Mapper接口方法

    List<User> getUserListLike(@Param("name") String name);

    xml配置文件

    <select id="getUserListLike" resultType="org.example.User">
    <bind name="pattern" value="'%' + name + '%'" />
    SELECT * FROM user
    WHERE name LIKE #{pattern}
    </select>

    <bind>语句内部的值为OGNL表达式,具体可参考http://www.mybatis.org/mybatis-3/dynamic-sql.html bind部分

  • 使用SQL concat()函数

    <select id="getUserListLikeConcat" resultType="org.example.User">
    SELECT * FROM user WHERE name LIKE concat ('%', #{name}, '%')
    </select>

除了注入问题之外,这里还需要对用户的输入进行过滤,永久有通配符,否则在表中数据量中断的时候,假设用户输入为%%,会进行全表模糊查询,严重情况下可导致DOS ,参考http://www.tothenew.com/blog/sql-wildcards-is-your-application-safe/

IN条件

使用<foreach>#{}

Mapper接口方法

List<User> getUserListIn(@Param("nameList") List<String> nameList);

xml配置文件

<select id="selectUserIn" resultType="com.example.User">
SELECT * FROM user WHERE name in
<foreach item="name" collection="nameList"
open="(" separator="," close=")">
#{name}
</foreach>
</select>

具体可参考http://www.mybatis.org/mybatis-3/dynamic-sql.html foreach部分

极限语句

直接使用#{}即可

Mapper接口方法

List<User> getUserListLimit(@Param("offset") int offset, @Param("limit") int limit);

xml配置文件

<select id="getUserListLimit" resultType="org.example.User">
SELECT * FROM user limit #{offset}, #{limit}
</select>

0x04 JPA和休眠

介绍

JPA:

  • 全称Java持久性API
  • ORM(对象关系映射)持久层API,需要有具体的实现

更多请参考https://en.wikipedia.org/wiki/Java_Persistence_API

休眠:

  • JPA ORM实现

更多请参考http://hibernate.org/

说明

这里有一种错误的认识,使用了ORM框架,就不会有SQL注入。而实际上,在Hibernate中,支持HQL(Hibernate查询语言)和native sql查询,前者存在HQL注入,封装和之前JDBC存在相同的注入问题,来具体看一下

高品质

HQL查询例子

Query<User> query = session.createQuery("from User where name = '" + name + "'", User.class);
User user = query.getSingleResult();

这里的User为类名,和原生SQL类似,拼接会导致注入

正确的用法:

  • 位置参数(位置参数)

Query<User> query = session.createQuery("from User where name = ?", User.class);
query.setParameter(0, name);

  • 命名参数(命名参数)

Query<User> query = session.createQuery("from User where name = :name", User.class);
query.setParameter("name", name);

  • 命名参数列表(命名参数列表)

Query<User> query = session.createQuery("from User where name in (:nameList)", User.class);
query.setParameterList("nameList", Arrays.asList("lisi", "zhaowu"));

  • 类实例(JavaBean)

User user = new User();
user.setName("zhaowu");
Query<User> query = session.createQuery("from User where name = :name", User.class);
// User 类需要有 getName() 方法
query.setProperties(user);

本机SQL

存在SQL注入

String sql = "select * from user where name = '" + name + "'";
// deprecated
// Query query = session.createSQLQuery(sql);
Query query = session.createNativeQuery(sql);

使用参数绑定来设置参数值

String sql = "select * from user where name = :name";
// deprecated
// Query query = session.createSQLQuery(sql);
Query query = session.createNativeQuery(sql);
query.setParameter("name", name);

JPA

JPA中使用JPQL(Java持久性查询语言),同时也支持本地sql,因此和Hibernate存在类似的问题,这里就不再细说,注意到的可以参考[如何使用Java Persistence API修复SQL注入( JPA)

文章目录
  1. 1. 0x01简介
  2. 2. 0x02 JDBC
    1. 2.1. 介绍
    2. 2.2. 说明
  3. 3. 0x03 Mybatis
    1. 3.1. 介绍
    2. 3.2. 说明
    3. 3.3. 更多场景
  4. 4. 0x04 JPA和休眠
    1. 4.1. 介绍
    2. 4.2. 说明
      1. 4.2.1. 高品质
      2. 4.2.2. 本机SQL
    3. 4.3. JPA