前言 最近,在做一个项目,当项目完成交付时,银行客户对我们的产品安全提出了质疑,要求我们对产品系统进行安全检测,应要求我们利用 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 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) { 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
浏览器窗口返回结果: 此时我们发现,查出了所有的用户信息,仔细调试会发现执行了如下的 sql1 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) { 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 select * from sys_user where account like concat('%' , #{account}, '%' )select * from sys_user where account like '%' || #{account} || '%' 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 { 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); } @Override public String getParameter (String s) { String parameter = super .getParameter(s); parameter = stripSqlInject(parameter); return parameter; } private String stripSqlInject (String parameter) { if (!StringUtils.isEmpty(parameter)) { parameter=parameter.replaceAll("(?i)\\w*\\s*((\\%27)|(\\'))\\s*((\\%6F)|o|(\\%4F))((\\%72)|r|(\\%52))" , "" ); 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