1 前言
最近我们公司的找了一个专门做网络安全的公司,这个公司叫做启明星辰(不是做广告哈,从网上查的资料来看,在业界还是挺有名气的),对现有的所有项目进行一个渗透性测试,发现了很多存在不安全的问题,其中之一就是SQL注入问题。对于SQL注入问题,其实使用像MyBatis之类的框架还是很容易避免的。而我们其中恰恰 有一个老项目是使用的Hibernate,为了简便,通过获取SessionFactory,以SQL拼接的方式执行的。由于是老项目,这时候如果要将所有的之类写法都更正过来,工作量太大。虽然是内部访问项目,但是还得想办法解决才行。最考经考量,因为是内部项目,所以也就不花大力气全部整改了,只是编写一个过滤器,将一些SQL关键字进行过滤,发现有这类关键字,则不执行操作。
但是这样做的话,有些风险, 那就是很容易会出现逃逸的情况,而且业务上也许真正需要用到SQL中的关键字时,那执行就进行不下去了。
下面是对于SQL注入问题的一些解决办法,如果文中有不当或错误烦请同学们指出。
2 SQL注入简介
SQL注入就是客户端在向服务器发送请求的时候,sql命令通过表单提交或者url字符串拼接传递到后台持久层,最终达到欺骗服务器执行恶意的SQL命令。
3 一般防御方式
一般我们在项目实践过程中,会应用到以下几种方式,来防止SQL注入
- 前端表单进行参数格式控制
- 后台进行参数格式化,过滤所有涉及sql的非法字符
- 后台的持久层框架进行使用预编译的方式输入参数,如目前常用到的Spring jdbcTemplate, Hibernate 、Mybatis.
4. jdbcTemplate防止sql注入的方法
-
使用参数化sql代替字符串拼接(少参)
// 字符串拼接(不安全): jdbcTemplate.update("update tb_user set age = "+age+" where uuid = "+uuid); //参数化sql(安全): jdbcTemplate.update("update tb_user set age = ? where uuid = ?",new Object[]{age,uuid});
-
参数化,将参数进行数组打包注入(多参)
List<Object> obj = new ArrayList<Object>(); obj.add(name); obj.add(age); String sql = "update tb_user set name=?,age = ? where uuid = 4"; jdbcTemplate.update(sql,obj.toArray());
- 参数化,将参数进行map集合打包,指定参数注入(多参)
Map<String,Object> map = new HashMap<String,Object>(); String sql = "update tb_user set name=:name,age =:age where uuid = 4"; map.put("name",name); map.put("age",age); jdbcTemplate.update(sql,map)
- 参数化,使用预编译语句(少参,参数为单体对象)
String sql = "insert into tb_user(name,age) values (?,?)"; jdbcTemplate.update(sql, new PreparedStatementSetter() { @Override public void setValues(PreparedStatement ps) throws SQLException { ps.setString(1, "sixmonth"); ps.setInt(2, 18); }});
- 参数化,使用预编译语句,进行批处理更新(多参,参数为对象集合)
String sql = "insert into user(name,age) values (?,?)"; List<TbUser> userList = new ArrayList<User>();//此处为测试,使用空集合 jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { public void setValues(PreparedStatement ps, int i) throws SQLException { ps.setString(1, userList.get(i).getName()); ps.setInt(2, userList.get(i).getAge()); } @Override public int getBatchSize() { return userList.size(); }
5. Hibernate防止sql注入
Hibernate在操作数据库的时候,一般可以用4种参数绑定的方式来防止SQL注入
-
按参数名称绑定
在HQL语句中定义命名参数要用”:”开头,形式如下:
Query query=session.createQuery(“from tb_User user where user.name=:customername and user:customerage=:age ”); query.setString(“customername”,name); query.setInteger(“customerage”,age);
上面代码中用:customername和:customerage分别定义了命名参数customername和customerage,然后用Query接口的setXXX()方法设定名参数值,setXXX()方法包含两个参数,分别是命名参数名称和命名参数实际值。
-
按参数位置邦定
在HQL查询语句中用”?”来定义参数位置,形式如下
Query query=session.createQuery(“from tb_User user where user.name=? and user.age =? ”); query.setString(0,name); query.setInteger(1,age);
同样使用setXXX()方法设定绑定参数,只不过这时setXXX()方法的第一个参数代表邦定参数在HQL语句中出现的位置编号(由0开始编号),第二个参数仍然代表参数实际值。
注:在实际开发中,提倡使用按名称邦定命名参数,因为这不但可以提供非常好的程序可读性,而且也提高了程序的易维护性,因为当查询参数的位置发生改变时,按名称邦定名参数的方式中是不需要调整程序代码的
-
setParameter()方法
在Hibernate的HQL查询中可以通过setParameter()方法邦定任意类型的参数,如下代码
String hql=”from tb_User user where user.name=:customername ”; Query query=session.createQuery(hql); query.setParameter(“customername”,name,Hibernate.STRING);
如上面代码所示,setParameter()方法包含三个参数,分别是命名参数名称,命名参数实际值,以及命名参数映射类型。对于某些参数类型setParameter()方法可以更具参数值的Java类型,猜测出对应的映射类型,因此这时不需要显示写出映射类型,像上面的例子,可以直接这样写:
query.setParameter(“customername”,name);但是对于一些类型就必须写明映射类型,比如java.util.Date类型,因为它会对应Hibernate的多种映射类型,比如Hibernate.DATA或者Hibernate.TIMESTAMP
-
setProperties()方法
在Hibernate中可以使用setProperties()方法,将命名参数与一个对象的属性值绑定在一起,如下程序代码
User user=new User(); user.setName(“pansl”); user.setAge(80); Query query=session.createQuery(“from USER c where c.name=:name and c.age=:age ”); query.setProperties(user);
setProperties()方法会自动将customer对象实例的属性值匹配到命名参数上,但是要求命名参数名称必须要与实体对象相应的属性同名。
这里还有一个特殊的setEntity()方法,它会把命名参数与一个持久化对象相关联,如下面代码所示
User customer=(Customer)session.load(Customer.class,”1”); Query query=session.createQuery(“from Order order where order.customer=:customer ”); query. setEntity(“customer”,customer); List list=query.list();
面的代码会生成类似如下的SQL语句:
Select * from order where customer_ID=’1’;
6. Mybatis防止sql注入
Mybatis防止sql注入的方法我认为就简单的多了,没有Hibernate那样需要编写太多的代码。
Mybatis 被注入,是因为接收的数格式${parameter}所导致。例如:
select * from tb_stu where companyName like '%$name$%',
这样极易受到注入攻击。
因为”parameter”这样格式的参数会直接参与sql编译,从而不能避免注入攻击。但涉及到动态表名和列名时,经常会用到“{ parameter }”这样格式的参数会直接参与sql编译,从而不能避免注入攻击。但涉及到动态表名和列名时,经常会用到“parameter”这样格式的参数会直接参与sql编译,从而不能避免注入攻击。但涉及到动态表名和列名时,经常会用到“{xxx}”这样的参数格式去写,如不得已的情况,需要采取手工地做好过滤工作((如过滤器等)来防止sql注入攻击。
正确的防注入方式
<sql id="condition_where"> <isNotEmpty property="companyName" prepend=" and "> t1. companyName _name like #name# </isNotEmpty> </sql>
或者:
concat('%',#{abc,jdbcType=VARCHAR},'%')
7. 过滤器过滤SQL关键字
这一种方式只能是由于需要修改的太多,做为一种补救方式进行修复。前面也提到,这样的方式很有可能会造成逃逸,或者影响正常的业务操作不能执行。
编写一个过滤器:
public class SqlInjectFilter extends HttpServlet implements Filter {
private static final long serialVersionUID = -2245859429480466089L;
FilterConfig filterConfig = null;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse res = (HttpServletResponse) response;
try {
// 获得所有请求参数名
Enumeration params = req.getParameterNames();
String sql = "";
while (params.hasMoreElements()){
// 得到参数名
String name = params.nextElement().toString();
// 得到参数对应值
String[] value = req.getParameterValues(name);
for (int i = 0; i < value.length; i++){
sql = sql + value[i];
}
}
// 截取我要拦截的字段
//sql = getfilte(sql);
// 有sql关键字,跳转到error.html
if (sqlValidate(sql)) {
res.setHeader("SESSIONSTATUS", "TIMEOUT");
res.setHeader("CONTEXTPATH", req.getContextPath() + "/templates/zh_cn/shop/error.html");
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
return;
// throw new IOException("您发送请求中的参数中含有非法字符");
}
chain.doFilter(request, response);
}
catch (Exception e){
e.printStackTrace();
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.filterConfig = filterConfig;
}
@Override
public void destroy() {
this.filterConfig = null;
}
// 效验, 这里过滤关键字,这些关键字是针对了 SQL Server数据库的
protected static boolean sqlValidate(String str){
str = str.toLowerCase();// 统一转为小写
String badStr = "drop |delete | or |waitfor|delay|";// 过滤掉的sql关键字,可以手动添加
String[] badStrs = badStr.split("\\|");
for (int i = 0; i < badStrs.length; i++){
// 循环检测,判断在请求参数当中是否包含SQL关键字
if (str.indexOf(badStrs[i]) >= 0){
return true;
}
}
return false;
}
// 截取需要拦截的字段
protected static String getfilte(String str) {
int idex = str.indexOf("filter");
String ss = "";
if (idex < 0) {
return ss;
}
if (str.indexOf(",", idex) < 0) {
ss = str.substring(idex, str.length());
} else {
ss = str.substring(idex, str.indexOf(",", idex));
}
return ss;
}
}