JDBC存在两种方法执行SQL语句,分别为PreparedStatement和Statement,相比Statement ,PreparedStatement会对SQL语句进行预编译,Statement会直接拼接sql语句造成SQL注入漏洞
示例代码中直接使用statement.executeQuery(sql),存在sql注入漏洞
@RequestMapping("/jdbc/vuln") public String jdbc_sqli_vul(@RequestParam("username") String username) { StringBuilder result = new StringBuilder(); try { Class.forName(driver); Connection con = DriverManager.getConnection(url, user, password); // sqli vuln code Statement statement = con.createStatement(); String sql = "select * from users where username = '" + username + "'"; logger.info(sql); ResultSet rs = statement.executeQuery(sql); while (rs.next()) { String res_name = rs.getString("username"); String res_pwd = rs.getString("password"); String info = String.format("%s: %s\n", res_name, res_pwd); result.append(info); logger.info(info); } rs.close(); con.close(); } catch (ClassNotFoundException e) { logger.error("Sorry,can`t find the Driver!"); } catch (SQLException e) { logger.error(e.toString()); } return result.toString(); }
此处存在sql注入漏洞,执行SQL语句为select * from users where username = 'joychoun' or '1' = '1'
与Statement的区别在于PrepareStatement会对SQL语句进行预编译,预编译的好处不仅在于在一定程度上防止了sql注入,还减少了sql语句的编译次数,提高了性能,其原理是先去编译sql语句,无论最后输入为何,预编译的语句只是作为字符串来执行,而SQL注入只对编译过程有破坏作用,执行阶段只是把输入串作为数据处理,不需要再对SQL语句进行解析,因此解决了注入问题。
PrepareStatement防御预编译的写法是使用?作为占位符然后将SQL语句进行预编译,由于"?"作为占位符已经告诉数据库整个SQL语句的结构,即?处传入的是参数,而不会是sql语句,所以即使攻击者传入sql语句也不会被数据库解析。
@RequestMapping("/jdbc/sec") public String jdbc_sqli_sec(@RequestParam("username") String username) { StringBuilder result = new StringBuilder(); try { Class.forName(driver); Connection con = DriverManager.getConnection(url, user, password); if (!con.isClosed()) System.out.println("Connecting to Database successfully."); // fix code String sql = "select * from users where username = ?"; PreparedStatement st = con.prepareStatement(sql); st.setString(1, username); logger.info(st.toString()); // sql after prepare statement ResultSet rs = st.executeQuery(); while (rs.next()) { String res_name = rs.getString("username"); String res_pwd = rs.getString("password"); String info = String.format("%s: %s\n", res_name, res_pwd); result.append(info); logger.info(info); } rs.close(); con.close(); } catch (ClassNotFoundException e) { logger.error("Sorry, can`t find the Driver!"); e.printStackTrace(); } catch (SQLException e) { logger.error(e.toString()); } return result.toString(); }
此时不存在SQL注入漏洞,传入的payload已进行转义
PreparedStatement只有在使用"?"作为占位符才能预防sql注入,直接拼接仍会存在sql注入漏洞
删除语句中可能会存在此类语句,由于无法确定delIds含有对象个数而直接拼接sql语句,造成sql注入。
String sql = "delete from users where id in("+delIds+"); //存在sql注入
解决方法为遍历传入的 对象个数,使用“?”占位符。
使用like语句直接拼接会造成sql注入
String sql = "select * from users where password like '%" + con + "%'"; //存在sql注入
没有手动过滤%。预编译是不能处理这个符号的, 所以需要手动过滤,否则会造成慢查询,造成 dos。
通过上面对使用in关键字和like关键字发现,只需要对要传参的位置使用占位符进行预编译时似乎就可以完全防止SQL注入,然而事实并非如此,当使用order by语句时是无法使用预编译的,原因是order by子句后面需要加字段名或者字段位置,而字段名是不能带引号的,否则就会被认为是一个字符串而不是字段名,然而使用PreapareStatement将会强制给参数加上',所以,在使用order by语句时就必须得使用拼接的Statement,所以就会造成SQL注入,需要进行手动过滤,否则存在sql注入。
String sql = "Select * from news where title =?" + "order by '" + time + "' asc"
Mybatis使用parameterType向sql语句传参,在sql引用传参可以使用#{Parameter}和${Parameter}两种方式
${Parameter}采用拼接的方式构造SQL语句,在对用户输入过滤不严格的前提下,存在sql注入漏洞
//Mybatis @Select("select * from users where username = '${username}'") List<User> findByUserNameVuln01(@Param("username") String username);
@GetMapping("/mybatis/vuln01") public List<User> mybatisVuln01(@RequestParam("username") String username) { return userMapper.findByUserNameVuln01(username); }
#{Parameter}采用预编译的方式构造SQL语句,避免了SQL注入的产生
@GetMapping("/mybatis/sec01") public User mybatisSec01(@RequestParam("username") String username) { return userMapper.findByUserName(username); }
//Mybatis @Select("select * from users where username = #{username}") User findByUserName(@Param("username") String username);
在这种情况下使用#{}程序会报错,新手程序员就把#号改成了$,这样如果java代码层面没有对用户输入的内容做处理势必会产生SQL注入漏洞。
<select id="findByUserNameVuln02" parameterType="String" resultMap="User"> select * from users where username like '%${_parameter}%' </select>
正确写法如下:
<select id="findByUserNamesec" parameterType="String" resultMap="User"> select * from users where username like concat('%',#{_parameter}, '%') </select>
这样拼接就不会存在sql注入。
正确写法:
mysql: select * from users where username like concat('%',#{username},'%') oracle: select * from users where username like '%'||#{username}||'%' sqlserver: select * from users where username like '%'+#{username}+'%'
使用in语句时直接使用#{}会报错,可能会存在使用${}直接拼接,造成sql注入
<select id="findByUserNameVuln04" parameterType="String" resultMap="User"> select * from users where id in (${id}) </select>
// http://localhost:8080/sqli/mybatis/vuln04?id=1)%20AND%201=1%23 @GetMapping("/mybatis/vuln04") public List<User> mybatisVuln04(@RequestParam("id") String id) { return userMapper.findByUserNameVuln04(id); }
正确用法为使用foreach,而不是将#替换为$
id in<foreach collection="ids" item="item" open="("separatosr="," close=")">#{ids} </foreach>
和JDBC同理,使用#{}方式传参会导致order by语句失效,所以使用order by语句的时候还是需要做好过滤
与SpringDataJpa类似,mybatis-plus提供了相关的funciton进行sql的操作,例如like("name","tks")——>name like '%tks%',同时也很贴心的考虑到了SQL注入问题,对绝大部分场景进行了预编译处理。但是类似动态表名、orderby这种需要拼接的场景在实际开发中还是需要额外的注意。
条件构造器Wrapper可以用于复杂的数据库操作:大于、小于、模糊查询等等。
比较常用的是QueryWrapper和UpdateWrapper:
配置基础类
import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data @TableName("tutorials") //tutorials public class Tutorial { private Integer Id; private String Title; private String Author; }
配置Mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.joychou.dao.Tutorial; @Mapper public interface TutorialMapper extends BaseMapper<Tutorial> { }
调用相关的api进行操作
@RequestMapping("selectauthor") public Tutorial mybatisselect(String author) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.eq("author",author); Tutorial tutorial = tutorialMapper.selectOne(wrapper); return tutorial; }
传统的mybaits框架对于in范围查询和like模糊查询需要做额外的处理:
like模糊查询需要在mapperxml配置中用sql的内置函数进行拼接,拼接后再采用#预编译的方式进行查询;
in范围查询的话需要在进行同条件多值查询的时候,可以使用MyBatis自带的循环指令foreach来解决SQL语句动态拼接的问题;
mybatis-puls已经考虑到sql注入的影响,相关wrapper的function已进行了相关的预编译处理。例如mybatis常见的like和in注入场景,均进行了预编译处理,例如如下例子:
like模糊查询
@RequestMapping("/like") public List<Tutorial> mybatislike(String author) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.select("*").like("author",author); List<Tutorial> tutorials = tutorialMapper.selectList(wrapper); return tutorials; }
打印相关的查询log,可以看到相关查询已使用?进行预编译处理:
in范围查询
@RequestMapping("/in") public List<Tutorial> mybatisin(String author1,String author2) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.select("*").in("author",author1,author2); List<Tutorial> tutorials = tutorialMapper.selectList(wrapper); return tutorials; }
打印相关的查询log,可以看到相关查询已使用?进行预编译处理:
apply(String applySql, Object... params) apply(boolean condition, String applySql, Object... params)
params
对应前面applySql
内部的{index}
部分.这样是不会有sql注入风险的,反之会有! apply()直接拼接sql语句存在sql注入
@RequestMapping("/applyvuln") public Tutorial mybatisapplyvuln(String author,String title) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.eq("author",author).apply("title="+title); Tutorial tutorial = tutorialMapper.selectOne(wrapper); return tutorial; }
使用{index}params进行预编译处理,不存在sql注入
@RequestMapping("/applysec") public Tutorial mybatisapplysec(String author,String title) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.eq("author",author).apply("title={0}",title); Tutorial tutorial = tutorialMapper.selectOne(wrapper); return tutorial; }
last(String lastSql) last(boolean condition, String lastSql)
@RequestMapping("/lastvuln") public List<Tutorial> mybatislastvuln(String column) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.last("order by " + column); List<Tutorial> list = tutorialMapper.selectList(wrapper); return list; }
exists(String existsSql) exists(boolean condition, String existsSql)
notExists(String notExistsSql) notExists(boolean condition, String notExistsSql)
拼接EXISTX / NOT EXISTS ( sql语句 )
注:若existsSql或notExistsSql中有关内容用户可控,则存在sql注入风险
示例:
@RequestMapping("/existsvuln") public List<Tutorial> mybatisexistsvuln(String title) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.exists("select title from tutorials where title = " + title); List<Tutorial> list = tutorialMapper.selectList(wrapper); return list; }
having(String sqlHaving, Object... params) having(boolean condition, String sqlHaving, Object... params)
@RequestMapping("/havingvuln") public List<Tutorial> mybatishavingvuln(String id) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.select().groupBy("author").having("id > " +id); List<Tutorial> list = tutorialMapper.selectList(wrapper); return list; }
orderBy(boolean condition, boolean isAsc, R... columns)
orderByAsc(R... columns) orderByAsc(boolean condition, R... columns)
orderByDesc(R... columns) orderByDesc(boolean condition, R... columns)
@RequestMapping("/orderbyvuln") public List<Tutorial> mybatishorderbyvuln(String column) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.select().orderBy(true, true, column); List<Tutorial> list = tutorialMapper.selectList(wrapper); return list; } @RequestMapping("/orderbyAscvuln") public List<Tutorial> mybatishorderbyAscvuln(String column) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.select().orderByAsc(column); List<Tutorial> list = tutorialMapper.selectList(wrapper); return list; } @RequestMapping("/orderbyDescvuln") public List<Tutorial> mybatishorderbyDescvuln(String column) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.select().orderByDesc(column); List<Tutorial> list = tutorialMapper.selectList(wrapper); return list; }
groupBy(R... columns) groupBy(boolean condition, R... columns)
@RequestMapping("/gropbycvuln") public List<Tutorial> mybatishsgropbycvuln(String column) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.select().groupBy(column); List<Tutorial> list = tutorialMapper.selectList(wrapper); return list; }
inSql(R column, String inValue) inSql(boolean condition, R column, String inValue)
notInSql(R column, String inValue) notInSql(boolean condition, R column, String inValue)
@RequestMapping("/insqlcvuln") public List<Tutorial> mybatisinsqlvuln(String column,String id) { QueryWrapper<Tutorial> wrapper = new QueryWrapper<>(); wrapper.select().inSql(column,"select id from tutorials where id >" + id); List<Tutorial> list = tutorialMapper.selectList(wrapper); return list; }
Wrapper提供了自定义SQL场景,与传统的mybatis一样使用$进行注解,但实际上ew已经做了预编译处理。同样的也支持注解&xml配置。
注:需要mybatis-plus版本 >= 3.0.7 param 参数名要么叫ew,要么加上注解@Param(Constants.WRAPPER) 使用${ew.customSqlSegment} 不支持 Wrapper 内的entity生成where语句
示例:
import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.joychou.dao.Person; import java.util.List; @Mapper public interface PersonMapper { @Select("select * from persons ${ew.customSqlSegment}") List<Person> selectPerson(@Param(Constants.WRAPPER) Wrapper wrapper); }
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.joychou.dao.Person; import org.joychou.dao.Tutorial; import org.joychou.mapper.PersonMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/sqlimybatis_plus") public class Mybatis_Plus_SQLI_2 { @Autowired PersonMapper personMapper; //orderby R... columns 可控,存在SQL注入风险 @RequestMapping("/selectperson") public List<Person> selectPerson(String column){ QueryWrapper<Object> wrapper = new QueryWrapper<>(); wrapper.orderByAsc(column); return personMapper.selectPerson(wrapper); } //like 自动进行预编译,不存在SQL注入风险 @RequestMapping("/selectpersonlike") public List<Person> selectPersonlike(String name) { QueryWrapper<Object> wrapper = new QueryWrapper<>(); wrapper.like("name", name); return personMapper.selectPerson(wrapper); } }
Wrapper自定义模式下like、in等会自动进行预编译,但若存在last、orderby等未进行预编译方法,若相应数据用户可控仍会存在SQL注入风险,风险API仍为2.1中常见注入场景。
import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.joychou.dao.Person; import java.util.List; @Mapper public interface PersonMapper { @Select("select * from persons ${ew.customSqlSegment}") List<Person> selectPerson(@Param(Constants.WRAPPER) Wrapper wrapper); List<Person> selectPersonXML1(Wrapper ew); List<Person> selectPersonXML2(Wrapper ew); List<Person> selectPersonXMLOrdeyBy(Wrapper ew); }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.joychou.mapper.UserMapper"> <resultMap type="org.joychou.dao.Person" id="Person"> <id column="id" property="id" javaType="java.lang.Integer" jdbcType="NUMERIC"/> <id column="name" property="name" javaType="java.lang.String" jdbcType="VARCHAR"/> <id column="address" property="address" javaType="java.lang.String" jdbcType="VARCHAR"/> <id column="city" property="city" javaType="java.lang.String" jdbcType="VARCHAR"/> </resultMap> <select id="selectPersonXML1" resultMap="Person"> select * from persons where ${ew.customSqlSegment} </select> <select id="selectPersonXML2" resultMap="Person"> select * from persons <where> ${ew.SqlSegment} </where> </select> <select id="selectPersonXMLOrdeyBy" resultMap="Person"> select * from persons ${ew.SqlSegment} </select> </mapper>
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.joychou.dao.Person; import org.joychou.dao.Tutorial; import org.joychou.mapper.PersonMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/sqlimybatis_plus") public class Mybatis_Plus_SQLI_2 { @Autowired PersonMapper personMapper; @RequestMapping("/selectperson") public List<Person> selectPerson(String column){ QueryWrapper<Object> wrapper = new QueryWrapper<>(); wrapper.orderByAsc(column); return personMapper.selectPerson(wrapper); } @RequestMapping("/selectperson1") public List<Person> selectPersonXML1(String name) { QueryWrapper<Object> wrapper = new QueryWrapper<>(); wrapper.eq("name", name); return personMapper.selectPerson(wrapper); } @RequestMapping("/selectperson2") public List<Person> selectPersonXML2(String name) { QueryWrapper<Object> wrapper = new QueryWrapper<>(); wrapper.eq("name", name); return personMapper.selectPerson(wrapper); } @RequestMapping("/selectperson3") public List<Person> selectPersonXMLOrderBy(String column) { QueryWrapper<Object> wrapper = new QueryWrapper<>(); wrapper.orderByAsc(column); return personMapper.selectPerson(wrapper); } }
以上selectPersonXML1、selectPersonXML2均实现相同功能,SQL注入问题与注解模式相同。
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisPlusConfig { /** * 注册插件 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(); // 设置请求的页面大于最大页后操作,true调回到首页,false继续请求。默认false pageInterceptor.setOverflow(false); // 单页分页条数限制,默认无限制 pageInterceptor.setMaxLimit(500L); // 设置数据库类型 pageInterceptor.setDbType(DbType.MYSQL); interceptor.addInnerInterceptor(pageInterceptor); return interceptor; } }
@RequestMapping("/selectpage") public List<Person> mybatispluspage(Long page,Long size,String order){ QueryWrapper<Person> qw = new QueryWrapper<>(); Page<Person> personPage = new Page<>(page,size); personPage.addOrder(OrderItem.asc(order)); IPage<Person> iPage = personMapper.selectPage(personPage, qw); List<Person> persons = iPage.getRecords(); return persons; }
若直接使用addOrder()未进行过滤,则存在SQL注入漏洞。
因为Order by排序时不能进行预编译处理,所以在使用插件时需要额外注意如下function,同样会存在SQL注入风险:
Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。
Hibernate可以使用hql来执行SQL语句,也可以直接执行SQL语句,无论是哪种方式都有可能导致SQL注入
hql语句:
String hql = "from People where username = '" + username + "' and password = '" + password + "'";
这种拼接方式存在SQL注入
正确使用以下几种HQL参数绑定的方式可以有效避免注入的产生:
Query<User> query = session.createQuery("from users name = ?1", User.class); String parameter = "g1ts"; Query<User> query = session.createQuery("from users name = :name", User.class); query.setParameter("name", parameter);
String parameter = "g1ts"; Query<User> query = session.createQuery("from users name = ?1", User.class); query.setParameter(1, parameter);
List<String> names = Arrays.asList("g1ts", "g2ts"); Query<User> query = session.createQuery("from users where name in (:names)", User.class); query.setParameter("names", names);
user1.setName("g1ts"); Query<User> query = session.createQuery("from users where name =:name", User.class); query.setProperties(user1);
这种方式是最常用,而且容易忽视且容易被注入的,通常做法就是对参数的特殊字符进行过滤,推荐大家使用 Spring工具包的StringEscapeUtils.escapeSql()方法对参数进行过滤:
import org.apache.commons.lang.StringEscapeUtils; public static void main(String[] args) { String str = StringEscapeUtils.escapeSql("'"); System.out.println(str); }
Hibernate支持使用原生SQL语句执行,所以其风险和JDBC是一致的,直接使用拼接的方法时会导致SQL注入
语句如下:
Query<People> query = session.createNativeQuery("select * from user where username = '" + username + "' and password = '" + password + "'");
正确写法:
String parameter = "g1ts"; Query<User> query = session.createNativeQuery("select * from user where name = :name"); query.setParameter("name",parameter);
注:此文为总结多位师傅文章,包括不限于以下文章:
https://www.sec-in.com/article/1073
https://www.cnblogs.com/klslb/p/7146889.html
.......