渗透性测试 - SQL注入问题的解决方法总结

本文探讨了SQL注入问题在老项目中如何通过Hibernate和MyBatis框架避免,以及使用过滤器作为临时解决方案的风险。重点介绍了JdbcTemplate、Hibernate的参数绑定和MyBatis的正确参数格式化方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 前言

​ 最近我们公司的找了一个专门做网络安全的公司,这个公司叫做启明星辰(不是做广告哈,从网上查的资料来看,在业界还是挺有名气的),对现有的所有项目进行一个渗透性测试,发现了很多存在不安全的问题,其中之一就是SQL注入问题。对于SQL注入问题,其实使用像MyBatis之类的框架还是很容易避免的。而我们其中恰恰 有一个老项目是使用的Hibernate,为了简便,通过获取SessionFactory,以SQL拼接的方式执行的。由于是老项目,这时候如果要将所有的之类写法都更正过来,工作量太大。虽然是内部访问项目,但是还得想办法解决才行。最考经考量,因为是内部项目,所以也就不花大力气全部整改了,只是编写一个过滤器,将一些SQL关键字进行过滤,发现有这类关键字,则不执行操作。

​ 但是这样做的话,有些风险, 那就是很容易会出现逃逸的情况,而且业务上也许真正需要用到SQL中的关键字时,那执行就进行不下去了。

下面是对于SQL注入问题的一些解决办法,如果文中有不当或错误烦请同学们指出。

2 SQL注入简介

SQL注入就是客户端在向服务器发送请求的时候,sql命令通过表单提交或者url字符串拼接传递到后台持久层,最终达到欺骗服务器执行恶意的SQL命令。

3 一般防御方式

一般我们在项目实践过程中,会应用到以下几种方式,来防止SQL注入

  1. 前端表单进行参数格式控制
  2. 后台进行参数格式化,过滤所有涉及sql的非法字符
  3. 后台的持久层框架进行使用预编译的方式输入参数,如目前常用到的Spring jdbcTemplate, Hibernate 、Mybatis.
4. jdbcTemplate防止sql注入的方法
  1. 使用参数化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});
    
    
  2. 参数化,将参数进行数组打包注入(多参)

      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());
    
    1. 参数化,将参数进行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)
    
    
    1. 参数化,使用预编译语句(少参,参数为单体对象)
       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);  
       }});
    
    
    1. 参数化,使用预编译语句,进行批处理更新(多参,参数为对象集合)
       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注入

  1. 按参数名称绑定

    在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()方法包含两个参数,分别是命名参数名称和命名参数实际值。

  2. 按参数位置邦定

    在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开始编号),第二个参数仍然代表参数实际值。

    注:在实际开发中,提倡使用按名称邦定命名参数,因为这不但可以提供非常好的程序可读性,而且也提高了程序的易维护性,因为当查询参数的位置发生改变时,按名称邦定名参数的方式中是不需要调整程序代码的

  3. 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

  4. 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编译,从而不能避免注入攻击。但涉及到动态表名和列名时,经常会用到“parametersql{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;
	}

}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值