SQL注入漏洞是对数据库进行的一种攻击方式。
其主要形成方式是在数据交互中,前端数据通过后台在对数据库进行操作时,由于没有做好安全防护,导致攻击者将恶意代码拼接到请求参数中,被当做SQL语句的一部分进行执行,最终导致数据库被攻击。可以说所有可以涉及到数据库增删改查的系统功能点都有可能存在SQL注入漏洞。虽然现在针对SQL注入的防护层出不穷,但大多情况下由于开发人员的疏忽或特定的使用场景,还是会存在SQL注入漏洞的代码。
一、Jdbc中SQL注入
1、动态拼接
SQL语句动态拼接导致的SQL注入漏洞是先前最为常见的场景。
其主要原因是后端代码将前端获取的参数动态直接拼接到SQL语句中使用 java.sql.Statement 执行
SQL语句从而导致SQL注入漏洞的出现。
在这里关键点有两个:①、动态拼接参数。②、使用 java.sql.Statement 执行SQL语句。
1.1、java.sql.Statement
Statement 对象用于执行一条静态的 SQL 语句并获取它的结果。
createStatement() :创建一个 Statement 对象,之后可使用 executeQuery() 方法执行SQL语句。
executeQuery(String sql) 方法:执行指定的 SQL 语句,返回单个 ResultSet 对象。
2、错误的预编译
在动态拼接中是使用Statement执行SQL语句。如果使用 PreparedStatement 预编译参数化查询是能够
有效防止SQL注入的。
但如果没有正确的使用 PreparedStatement 预编译还是会存在SQL注入风险的。
2.1、java.sql.PreparedStatement
PreparedStatement是继承Statement的子接口。
PreparedStatement 会对SQL语句进行预编译,不论输入什么,经过预编译后全都以字符串来执行SQL
语句。
PreparedStatement会先使用 ? 作为占位符将SQL语句进行预编译,确定语句结构,再传入参数进行执
行查询。如下述代码:
String sql = "select * from users where username = ?";
PreparedStatement preparestatement = conn.prepareStatement(sql);
preparestatement.setString(1, username);
3.order by注入
在SQL语句中, order by 语句用于对结果集进行排序。 order by 语句后面需要是字段名或者字段位
置。
在使用 PreparedStatement 预编译时,会将传递任意参数使用单引号包裹进而变为了字符串。
如果使用预编译方式执行 order by 语句,设置的字段名会被人为是字符串,而不在是字段名。
因此,在使用 order by 时,就不能使用 PreparedStatement 预编译了。
二、Mybatis
1、Mybatis中 #{} 和 ${} 区别
在Mybatis中拼接SQL语句有两种方式:一种是占位符 #{} ,另一种是拼接符 ${} 。
占位符 #{} :对传入的参数进行预编译转义处理。类似JDBC中的 PreparedStatement 。
比如: select from user where id = #{number} ,如果传入数值为1,最终会被解析成 select from user where id = "1" 。
拼接符 ${} :对传入的参数不做处理,直接拼接,进而会造成SQL注入漏洞。
比如: select * from user where id = ${number} ,如果传入数值为1,最终会被解析成
select * from user where id = 1 。
#{} 可以有效防止SQL注入漏洞。 ${} 则无法防止SQL注入漏洞。
因此在我们对JavaWeb整合Mybatis系统进行代码审计时,应着重审计SQL语句拼接的地方。
除非开发人员的粗心对拼接语句使用了 ${} 方式造成的SQL注入漏洞。
在Mybatis中有几种场景是不能使用预编译方式的,比如: order by 、 in , like 。
2、order by注入
ORDER BY语句 :用于对查询结果的排序,asc为升序,desc为降序。默认为升序。
比如: select * from users order by username asc;
与JDBC预编译中order by注入一样,在 order by 语句后面需要是字段名或者字段位置。因此也不能使
用Mybatis中预编译的方式。
3、in注入
IN语句 :常用于where表达式中,其作用是查询某个范围内的数据。
比如: select * from where field in (value1,value2,value3,…);
如上所示,in在查询某个范围数据是会用到多个参数,在Mybtis中如果直接使用占位符 #{} 进行查询会
将这些参数看做一个整体,查询会报错。
因此很多开发人员可能会使用拼接符 ${} 对参数进行查询,从而造成了SQL注入漏洞。
比如: select * from users where id in (${params})
正确的做法是需要使用foreach配合占位符 #{} 实现IN查询。比如:
<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name IN
<foreach collection="names" item="name" open="(" close=")" separator=",">
#{name}
</foreach>
</select>
4、like注入
LIKE 语句 :在一个字符型字段列中检索包含对应子串的。
比如: select * from users where username like admin
使用like语句进行查询时如果使用占位符 #{} 查询时程序会报错(大家可自行调试)。
比如: select * from users where username like '%#{username}%'
因此经验不足的开发人员可能会使用拼接符 ${} 对参数进行查询,从而造成了SQL注入漏洞。
比如: select * from users where username like '%${username}%'
下面代码是正确的做法,可以防止SQL注入漏洞,如下。
SELECT * FROM users WHERE name like CONCAT("%", #{name}, "%")
三、SQL注入漏洞修复
原文:https://gist.github.com/retanoj/5fd369524a18ab68a4fe7ac5e0d121e8
3.1:表,字段名称
(Select, Order by, Group by 等)
♾️ java 代码:// 插入数据用户可控时,应使用白名单处理
// example for order by
String orderBy = "{user input}";
String orderByField;
switch (orderBy) {
case "name":
orderByField = "name";break;
case "age":
orderByField = "age"; break;
default:
orderByField = "id";
}
3.2:JDBC
♾️ java 代码:String name = "foo";
// 一般查询场景
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, name);
ResultSet rs = pre.executeQuery();
// like 模糊查询场景
String sql = "SELECT * FROM users WHERE name like ?";
PreparedStatement pre = conn.prepareStatement(sql);
pre.setString(1, "%"+name+"%");
ResultSet rs = pre.executeQuery();
// where in 查询场景
String sql = "select * from user where id in (";
Integer[] ids = new Integer[]{1,2,3};
StringBuilder placeholderSql = new StringBuilder(sql);
for(int i=0,size=ids.length;i<size;i++) {
placeholderSql.append("?");
if (i != size-1) {
placeholderSql.append(",");
}
}
placeholderSql.append(")");
PreparedStatement pre = conn.prepareStatement(placeholderSql.toString());
for(int i=0,size=ids.length;i<size;i++) {
pre.setInt(i+1, ids[i]);
}
ResultSet rs = pre.executeQuery();
3.3:Spring-JDBC
♾️ java 代码:JdbcTemplate jdbcTemplate = new JdbcTemplate(app.dataSource());
// 一般查询场景
String sql = "select * from user where id = ?";
Integer id = 1;
UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), id);
// like 模糊查询场景
String sql = "select * from user where name like ?";
String like_name = "%" + "foo" + "%";
UserDO user = jdbcTemplate.queryForObject(sql, BeanPropertyRowMapper.newInstance(UserDO.class), like_name);
// where in 查询场景
NamedParameterJdbcTemplate namedJdbcTemplate = new NamedParameterJdbcTemplate(app.dataSource());
MapSqlParameterSource parameters = new MapSqlParameterSource();
parameters.addValue("names", Arrays.asList("foo", "bar"));
String sql = "select * from user where name in (:names)";
List<UserDO> users = namedJdbcTemplate.query(sql, parameters, BeanPropertyRowMapper.newInstance(UserDO.class));
3.4:Mybatis XML Mapper
♾️ java 代码:<!-- 一般查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name = #{name}
</select>
<!-- like 查询场景 -->
<select id="select" parameterType="java.lang.String" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name like CONCAT("%", #{name}, "%")
</select>
<!-- where in 查询场景 -->
<select id="select" parameterType="java.util.List" resultMap="BaseResultMap">
SELECT *
FROM user
WHERE name IN
<foreach collection="names" item="name" open="(" close=")" separator=",">
#{name}
</foreach>
</select>
3.5:Mybatis Criteria
♾️ java 代码:public class UserDO {
private Integer id;
private String name;
private Integer age;
}
public class UserDOExample {
// auto generate by Mybatis
}
UserDOMapper userMapper = session.getMapper(UserDOMapper.class);
UserDOExample userExample = new UserDOExample();
UserDOExample.Criteria criteria = userExample.createCriteria();
// 一般查询场景
criteria.andNameEqualTo("foo");
// like 模糊查询场景
criteria.andNameLike("%foo%");
// where in 查询场景
criteria.andIdIn(Arrays.asList(1,2));
List<UserDO> users = userMapper.selectByExample(userExample);