web应用安全之SQL注入

sql injection

前言

最近,在做一个项目,当项目完成交付时,银行客户对我们的产品安全提出了质疑,要求我们对产品系统进行安全检测,应要求我们利用IBM AppScan 安全扫描工具进行了扫描,经过扫描我们发现系统存在一些SQL注入、XSS攻击等安全漏洞。我们在开发web应用的过程中,对于项目DAO层的SQL非法注入问题是我们经常会考虑的web安全隐患之一。作为一个从业多年的Java web应用开发者,本文将从java的角度来说说开发过程中的SQL注入的问题。

什么是SQL注入

所谓SQL注入,就是攻击者恶意将SQL命令插入到Web表单提交或输入域名或页面请求的查询字符串,这样当应用程序向后台数据库进行SQL查询时,以“欺骗”服务器执行非法的SQL命令,最终致使攻击者非法数据侵入系统。

现在我们通过一个简单的项目演示攻击者利用SQL注入非法入侵系统。

SQL注入演示

环境搭建

采用Mysql新建用户表,并搭建一个web项目。

SYS_USER
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- ----------------------------
-- Table structure for `SYS_USER`
-- ----------------------------
DROP TABLE IF EXISTS `SYS_USER`;
CREATE TABLE `SYS_USER` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`account` varchar(50) NOT NULL COMMENT '登录名',
`password` varchar(100) NOT NULL COMMENT '密码(加密)',
`lastLoginIp` varchar(20) DEFAULT NULL COMMENT '最后登录IP',
`lastLoginTime` datetime DEFAULT NULL COMMENT '最后登录时间',
`loginCount` int(11) NOT NULL COMMENT '登录总次数',
`createTime` datetime NOT NULL COMMENT '创建时间',
`isEnable` int(1) NOT NULL COMMENT '是否启用',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='用户表';

并插入数据,如下:
准备数据

controller层提供一个查询用户列表的接口(RESTFu风格)

UserController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Path("/user")
@Transactional
@Component
@Slf4j
public class UserController {
@Autowired
private UserDao userDao;

@Resource(name = "myDataSource")
private DataSource myDataSource;

@GET
@Path("/list")
@Produces(MediaType.APPLICATION_JSON)
public Response getUserList(@QueryParam("account") String account){
//执行SQL,输出查到的数据
JdbcTemplate jdbcTemplate = new JdbcTemplate(myDataSource);
String sql = "select * from sys_user where account ='"+account+"'";
List<?> resultList = jdbcTemplate.queryForList(sql);
return Response.ok(resultList).build();
}
}

注入演示

首先,我们在浏览器输入请求地址,发出请求,查询账户为admin的用户信息:

GET>http://localhost:8080/user/list?account=admin

浏览器窗口正常返回结果:
正常返回结果

接着,我们在发出这样一个请求,如下

GET> http://localhost:8080/user/list?account=admin’ or ‘a’='a

浏览器窗口返回结果:
异常结果
此时我们发现,查出了所有的用户信息,仔细调试会发现执行了如下的sql

1
select * from sys_user where account ='admin' or 'a'='a'

这是因为我们传入的参数account参数与我们接口中的查询语句进行拼接后构成了一条合法的SQL查询,这就是SQL注入。黑客往往就会通过传入精心构造的参数来进行SQL注入,非法入侵系统。

SQL注入的防范与处理

SQL注入原因就是由于传入的参数与系统的SQL拼接成了合法的SQL而导致的,而其本质还是将用户输入的数据当做了代码执行。了解了SQL注入的本质和原理,在Java web应用开发的过程中,我们如何防范和处理呢?

JDBC的预处理

Java的JDBC中,有个预处理功能,这个功能提供了PreparedStatement (预处理执行语句)的方式,SQL语句在程序运行前已经进行了预编译,在程序运行时第一次操作数据库之前,SQL语句已经被数据库分析,编译和优化,对应的执行计划也会缓存下来并允许数据库以参数化的形式进行查询,当运行时,动态地把参数传给PreprareStatement时,即使参数里有敏感字符,如 or ‘a=a’,数据库会将整个参数作为一个字段的属性值来处理而不会作为一个SQL指令,这样就在一定程度上预防了绝大多数的SQL注入。
对刚才的代码做优化,采用预处理的方式,如下:

UserController.java
1
2
3
4
5
6
7
8
9
10
@GET
@Path("/list")
@Produces(MediaType.APPLICATION_JSON)
public Response getUserListPreprareStatement(@QueryParam("account") String account){
//执行SQL,输出查到的数据
JdbcTemplate jdbcTemplate = new JdbcTemplate(myDataSource);
String sql = "select * from sys_user where account = ?";
List resultList = jdbcTemplate.queryForList(sql,account);
return Response.ok(resultList).build();
}

此时,我们再采用刚才的SQL非法注入的方式访问,发现未查询出任何数据,说明SQL注入未成功,打印JDBC预处理后的SQL,发现所有的 ’ 都被 ' 转义掉了,从而防止了SQL注入。

Mybatis下注入防范

Mybatis框架作为一款半自动化的持久层框架,支持定制化 SQL、存储过程以及高级映射,其sql语句都要我们自己来手动编写,使用该框架时,防止SQL注入我们只需要弄清楚#{}和${}的区别以及order by注入问题。

#{}:使用的是PreparedStatement,会有类型转换,比较安全;
{}:使用字符串拼接,可以SQL注入; order by语句后不能用#{},只能用{},此时会存在SQL注入危险,需要手动处理;
like查询不小心会有漏动,正确写法如下:

1
2
3
4
5
6
--Mysql:
select * from sys_user where account like concat('%', #{account}, '%')
--Oracle:
select * from sys_user where account like '%' || #{account} || '%'
--SQLServer:
select * from sys_user where account like '%' + #{account} + '%'

自定义过滤规则防范注入

由于动态SQL语句是引发SQL注入的根源。因此,开发过程中我们应尽量使用预编译语句来组装SQL查询,并且,随着ORM技术的发展,很多ORM框架在安全问题上都有进行处理,只要我们按照规范,基本上可以很大程度的消除SQL注入的风险。但是,在必要情况下,我们还需通过自定义过滤规则的方式来防范SQL注入。就Java web而言,我们可以通过在后台添加自定义的过滤器(Filter),对每个请求的参数过滤一些关键字,替换成安全的,从而解决注入问题,步骤如下

在后台添加自定义的过滤器,对每个请求进行过滤

SqlFilter.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SqlFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//在Filter里面调用自定义的HttpServletRequestWrapper
filterChain.doFilter(new SqlHttpServletRequestWrapper((HttpServletRequest) servletRequest), servletResponse);
}

@Override
public void destroy() {

}
}

实现一个自定义的HttpServletRequestWrapper,然后在Filter里面调用它,重写getParameter方法

SqlHttpServletRequestWrapper.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
public class SqlHttpServletRequestWrapper extends HttpServletRequestWrapper {

public SqlHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}

/**
* 重写getParameter方法 过滤一些关键字,替换成安全的
* @param s
* @return
*/
@Override
public String getParameter(String s) {
String parameter = super.getParameter(s);
parameter = stripSqlInject(parameter);
return parameter;
}

/**
* 对每个请求的参数过滤一些关键字,替换成安全的
* @param parameter
* @return
*/
private String stripSqlInject(String parameter) {
if(!StringUtils.isEmpty(parameter)) {
//干掉or 攻击
parameter=parameter.replaceAll("(?i)\\w*\\s*((\\%27)|(\\'))\\s*((\\%6F)|o|(\\%4F))((\\%72)|r|(\\%52))", "");
//干掉union 攻击
parameter=parameter.replaceAll("(?i)\\w*\\s*((\\%27)|(\\'))\\s*union", "");
//干掉截断攻击--原理:通过注释符号来截断后面的查询条件
parameter=parameter.replaceAll("(?i)\\s*((\\%27)|(\\'))[\\s\\S^-]*--\\s*[and|exec|execute|insert|select|delete|"
+ "update|count|drop|truncate|information_schema.columns|table_schema|union]*", "");
}
return parameter;
}
}

在web.xml中配置过滤器

web.xml
1
2
3
4
5
6
7
8
9
<filter>
<filter-name>SqlFilter</filter-name>
<filter-class>com.syshlang.framework.filter.SqlFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SqlFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
</filter-mapping>

附:本次演示的项目地址
https://github.com/syshlang/syshlang-injection-demo