当前位置: 首页 > 科技观察

如何避免SQL注入漏洞

时间:2023-03-19 18:10:40 科技观察

1.前言本文将阐述在开发过程中仍然频繁出现的SQL编码缺陷背后的原理和成因。并以几个常见漏洞的形式,提醒技术同学注意相关问题。最后会根据原理给出解决方案或缓解方案。2、SQL注入漏洞产生的原理及成因SQL注入漏洞的根本原因是将外部输入误认为是SQL代码来执行。目前最好的解决方案是预编译的方式。在SQL语句的执行过程中,需要进行以下三个基本步骤:代码语义分析制定执行计划,得到返回结果,一条SQL语句由代码和数据组成,如:SELECTid,name,phoneFROMuserTableWHEREname='xiaoming';SELECTid,name,phoneFROMuserTableWHEREname=是代码,'xiaoming'是数据。而预编译,以Mybatis为例,就是预解析带占位符的语义:比如SELECTid,name,phoneFROMuserTableWHEREid=#{name};然后将数据'xiaoming'传递到占位符符号中。这样代码语义分析阶段如果错开,就不会被误认为是部分代码。最早的时候,开发者显式地使用JDBC来创建Connections和执行SQL语句。在这种情况下,如果将外部可控的数据拼接到SQL语句中,没有经过充分的过滤,就会出现漏洞。这种情况在正常的业务发展过程中是很少见的。按照公司的规定,没有特殊情况必须使用ORM框架来执行SQL。但是目前在一些项目中还是使用JDBC来编写一些工具脚本,比如DataMerge.java、DatabaseClean.java,借用JDBC的灵活性,通过这些脚本来进行数据库的批量操作。这样的代码应该不会出现在网络版中,以免因各种情况被外部调用。三、直接使用Mybatis1、容易出错的点目前大部分平台代码都是基于Mybatis来处理持久层与数据库的交互。Mybatis的传入数据有两个占位符{}和#{}。{}和#{}。{}可以理解为语义分析前的字符串拼接,传入的参数原样传入。例如,SELECTid,name,phoneFROMuserTableWHEREname='${name}';传入name=xiaoming后,相当于SELECTid,name,phoneFROMuserTableWHEREname='xiaoming';实际应用中,SELECTid,name,phoneFROMuserTableWHERE${col}='xiaoming';传入col="name",相当于SELECTid,name,phoneFROMuserTableWHEREname='xiaoming';预编译原理介绍中提到,使用#{}占位符不存在注入问题。但是有些业务场景不能直接使用#{}。(1)比如你写SELECTid,name,phoneFROMuserTableORDERBY#{};在orderby语法中,执行时会报错。因为orderby后面的内容是列名,属于代码语义的一部分。如果在语义分析部分没有确定,相当于执行SELECTid,name,phoneFROMuserTableORDERBY。一定会有语法错误。(2)再比如,在like场景下,SELECTid,name,phoneFROMuserTableWHEREnamelike'%#{name}%';#{}不会被解析,导致报错。in语法和between语法都是一样的,那么这种问题怎么解决呢?2.正确的写法(1)在orderby(groupby)语句中使用${}来使用条件判断select*fromuserswhereid<#{id}orderbynameorderbyageorderbyid使用全局过滤机制,将orderby后的变量内容限制为只能是数字、字母、下划线。如使用正则过滤:keywordkeyword=keyword.replaceAll("[^a-zA-Z0-9_\s+]","");这里要注意,过滤需要使用白名单,不能使用黑名单,不能解决注入问题。(2)LIKE语句要求like中的关键字用两个%符号包裹起来,所以可以使用CONCAT函数进行拼接。SELECT*FROMstudentWHEREStudent.stu_nameLIKECONCAT('%',#{stuName},'%')注意不要使用CONCAT('%','${stuName}','%'),所以还是有漏洞。也就是说,使用$符号是错误的,使用#符号是安全的。(3)IN语句类似于like语句。如果直接使用#{},会报错。常见的错误是:tenant_idin(${tenantIds})正确的做法是:select*fromnewswhereidin#{item}四、Mybatis-generator使用安全,CRUD代码压力大,开发者慢慢开始使用Mybatis-generator、idea-mybatis-generatorplugin、generalMapper、Mybatis-generator-plus来自动生成Mapper、POJO、Dao和其他文件。这些工具可以自动生成CRUD需要的文件,但是如果使用不当,会自动生成SQL注入漏洞。下面以最常用的org.mybatis.generator为例,说明一下可能出现的问题。1、动态语句支持Mybatis-generator提供的一些功能,帮助用户连接各种SQL条件,比如多参数的like语法,多参数的比较语法。为了保证使用的简单性,需要将一些语义代码拼接成SQL语句。而且如果开发者使用不当,外部输入也会传入{}占位符。会有漏洞。2.targetRuntime参数配置配置generator时,在配置文件generator-rds.xml中有一个targetRuntime属性,默认为MyBatis3。此时会启动Mybatis的动态语句支持,启动enableSelectByExample、enableDeleteByExample、enableCountByExample和enableUpdateByExample函数。以enableSelectByExample为例,会在xml映射文件中代入以下动态模块:and${criterion.条件}和${criterion.condition}#{criterion.value}和${criterion.condition}#{criterion.value}and#{criterion.secondValue}and${criterion.condition}#{listItem}开发者可以通过包含模块来添加where条件,但是如果使用不当,会导致SQL注入漏洞:>fromuserorderby${orderByClause}<并添加带有自定义参数的函数:publicCriteriaaddKeywordTo(Stringkeyword){StringBuildersb=newStringBuilder();sb.append("(display_namelike'%"+keyword+"%'or");sb.append("orglike'"+keyword+"%'or");sb.append("statuslike'%"+keyword+"%'or");sb.append("idlike'"+keyword+"%')");addCriterion(sb.toString());return(Criteria)this;}目的是同时实现对display_name、org、status、id的like操作。addCriterion是Mybatis-generator自带的函数:protectedvoidaddCriterion(Stringcondition){if(condition==null){thrownewRuntimeException("Valueforconditioncannotbenull");}criteria.add(newCriterion(condition));}这里的误解是addCriterion本身提供了对多个条件的支持,但是开发者认为需要将多个条件拼接在一起,传入addCriterion方法。就像案例中的代码一样,addCriterion最终只传入了一个参数。从而执行Example_Where_Clause语句:和${criterion.condition}即开发者直接将自己拼接的SQL语句代入${criterion.condition},导致漏洞的产生。根据Mybatis-generator的文档,正确的写法应该是:publicvoidaddKeywordTo(Stringkeyword,UserExampleuserExample){userExample.or().andDisplayNameLike("%"+keyword+"%");userExample.or().andOrgLike(keyword+"%");userExample.or().andStatusLike("%"+keyword+"%");userExample.or().andIdLike("%"+keyword+"%");}或方法负责用于创建Criteria,此时触发的逻辑是将和${criterion.condition}#{criterion.value}${criterion.condition}换成like不带单引号,像语义代码一样,在语义分析之前拼接成SQL语句,预编译的#{criterion.value}中会添加“%”+关键字+“%”作为数据,从而避免注射。同样,也提供了In语法的安全用法:Listfield5Values=newArrayList();field5Values.add(8);field5Values.add(11);field5Values.add(14);field5Values.add(22);example.or().andField5In(field5Values);Beetween的安全使用:example.or().andField6Between(3,7);Mybatis-generator默认生成的orderby语句也是直接使用${}拼接的:它会导致注入问题。3.除了自己写的SQL语句外,Mybatis-generator默认生成的orderby语句也是直接使用${}拼接的:orderby${orderByClause}/if>如果没有对传入的参数进行额外的过滤,就会导致注入问题。PS:在实际扫雷过程中,发现很多语句自动生成orderby语法,但是上层调用的时候并没有传入这个可选参数。在这种情况下,冗余的语法顺序应该被删除。4.其他插件插件之间的安全漏洞不尽相同。下面简单列出几个常用的插件。(1)idea-mybatis-generator这是IDEA的一个插件,可以在开发过程中从IDE层面自动生成CRUD中需要的文件。使用此插件时,还需要注意一些默认的安全隐患。1)通过like\in\between的处理自定义order,可参考官方文档使用,无安全隐患。但是插件没有内置orderbyprocessing,需要自己写。写的时候参考Case22)默认的IF条件前需要判断是否为空。插件默认生成的语法大致如下:ID=#{ID}and当id参数为null时,不会在sql语句中加入if标签下的逻辑,可能导致DOS、权限绕过等漏洞。因此,在将参数传入查询语句之前,需要先确认它不为空。(2)com.baomidou.mybatis-plusapply方法中传递参数时,应使用{}自带的最后一个方法。原理是直接拼接在SQL语句末尾,存在注入漏洞。五。其他ORM框架1.HibernateORM全称为对象关系映射(ObjectRelationalMapping)。简单的说就是将数据库中的表映射为Java对象。这种只有属性,没有业务逻辑的对象也称为POJO(PlainOrdinaryJavaObject)对象。Hibernate是第一个广泛使用的ORM框架。它通过XML管理数据库连接,提供全表映射模型,封装度高。配置好映射文件和数据库链接文件后,Hibernate就可以通过Session对象进行数据库操作了。开发者无需接触SQL语句,只需要编写HQL语句即可。Hibernate经常与Struts和Spring结合使用,这是Java世界中经典的SSH框架。与SQL相比,HQL在语法上有很多限制:无法查询未映射的表,只有在模型之间的关系明确时才能使用UNION语法。表名、列名区分大小写。不*,#,-。没有延时功能。所以HQL注入的利用要比SQL注入难得多。从代码审计的角度来看,与普通SQL注入一致:拼接会导致注入漏洞:ListstudentList=session.createQuery("FROMStudentsWHEREs.stuId="+stuId).list();可以使用占位符和名称参数来阻止SQL语句,这些语句本质上是预编译的。ListstudentList=session.createQuery("FROMStudentsWHEREs.stuId=:stuId").setParameter("stuId",stuId).list();ListstudentList=session.createQuery("FROMStudentsWHEREs.stuId=?").setParameter(stuId).list();Hibernate在使用过程中有很多不足:全表映射不灵活,更新时需要发送所有字段,影响程序运行效率。对复杂查询的支持很差。对存储过程的支持很差。HQL性能较差,无法针对SQL进行优化。在审计Hibernate相关的注入时,可以通过全局搜索createQuery快速定位SQL操作的位置。2、JPAJPA全称为JavaPersistenceAPI,是JavaEE提供的一种数据持久化规范,允许开发者通过XML或注解的方式将一个对象持久化到数据库中。主要包括三个方面:(1)ORM映射元数据,通过XML或注解描述对象与数据表的对应关系。框架可以自动将对象中的数据保存到数据库中。常用注解有:@Entity、@Table、@Column、@Transient(2)数据操作API,内置接口,方便对数据表进行CRUD操作,节省开发者编写SQL的时间。常用方法有:entityManager.merge(Tt);(3)JPQL,它提供了一种面向对象而非面向数据库的查询语言,将程序与数据库和SQL解耦。JPA是一套规范,Hibernate就是实现了这个JPA规范。在Spring框架中,提供了一个简单版本的JPA实现——spirngdatajpa。按照约定的方法命名规则编写dao层接口,无需编写接口实现即可访问和操作数据库。同时,它提供了很多CRUD之外的功能,比如分页、排序、复杂查询等等。更易于使用,但仍然在底层使用Hibernate的JPA实现。和HQL注入一样,如果使用拼接的方式将用户可控的数据代入到查询语句中,就会导致SQL注入。安全查询应该使用预编译技术。SpringDataJPA的预编译写法为:StringgetUser="SELECTusernameFROMusersWHEREid=?";Queryquery=em.createNativeQuery(getUser);query.setParameter(1,id);Stringusername=query.getResultList();提示:其实Hibernate出现日期早于JPA规范。Hibernate逐渐成熟后,JavaEE开发团队邀请Hibernate核心开发人员制定JPA规范。之后根据规范进一步优化了SpringDataJPA。此外,还有很多产品是为JPA规范执行的,例如Eclipse的TopLink(OracleLink)。6.总结经过上面的介绍,特别是围绕Mybatis容易出错的点的讨论,我们可以得出以下结论:持久层组件有很多种。开发者对工具使用的误解是产生漏洞的主要原因。由于自动生成插件的动态特性,不能简单地使用${}自动发现SQL漏洞。必须根据全局持久层组件的特点制定详细的匹配规则。【本文为专栏作者《阿里巴巴官方技术》原创稿件,转载请联系原作者】点此查看作者更多好文