一、MyBaties中#{}和${}的区别
'#'相当于对数据 加上 双引号,$相当于直接显示数据。
我们经常使用的是#{},一般解说是因为这种方式可以防止SQL注入,简单的说#{}这种方式SQL语句是经过预编译的,它是把#{}中间的参数转义成字符串,举例:
select * from table_A where name = #{name}
预编译后,会动态解析成一个参数标记符?:
select * from table_A where name = ?
而使用${}在动态解析时候,会传入参数字符串
select * from table_A where name = 'xiaoming'
总结:
#{} 这种取值是编译好SQL语句再取值
${} 这种是取值以后再去编译SQL语句
因此#{}方式能够很大程度防止sql注入
二、存在SQL注入的情况以及修改方法
如果拼接参数是SQL的查询参数都可以通过将mybatis配置文件中的 $ 改为 #,来解决
where id=${id} -->
where id=#{id}
where id like '%${id}%' -->
where id like concat('%',#{id},'%')
where id in (${ids}) -->
where id in
<foreach item='id' index='index' collection='ids' open='(' seperator=',' close=')'>
#{id}
</foreach>
如果SQL中动态部分是攻击者可控的,那么就会形成SQL注入
SQL注入本质是数据和代码没有分离
围绕上面两点,防御的方案也就呼之欲出了。即,如果SQL中动态部分做到攻击者不可控,或者能够严格保证数据和代码是分离的。
- 防御思想
2.1 打破“攻击者可控”
这是通用的防御思路,不仅限于防御SQL注入,防御的思路和常用手段参考:《通读:WEB应用安全(二)》。
2.2 数据代码严格分离
我们在《通读:SQL注入(认识篇)》中说明SQL注入时,举了如下SQL:
SELECT xx,yy,...,FROM product WHERE name LIKE '%kindle' or 1+1=2 and '%'='%';
仔细观察,会发现用户输入的单引号跟SQL中的单引号闭合了,从而其他输入被当成了“代码”。朴素的想法是:如果我们能保证用户的输入一定是被单引号包裹,那么输入将是当作字符串(数据)对待。
但是依靠用户编写代码可能容易错漏。比如,顺着上面的朴素想法,我们可能想到策略:把用户输入的单引号转义(如使用反斜杠转义,’),那用户输入就无法跳出单引号。
然而反斜杠自身也可能被转义,导致其无法起到转义符的作用。
解决之道是将这些工作留给BD层,用户代码和BD层建立沟通。
用户代码:呐,A这个字段我告诉你是一个int,B这个字段是一个字符串…
DB层:好的,我来保证A的输入只能是int,B的输入只能是字符串。
这个机制由预编译、占位符来提供(而我们举例场景通常称之为“拼接”),不同的语言、框架实现略有差异,但是本质是一致的。
注:数据代码严格分离其实是打破“攻击者可控”的一种手段,两者不是并列关系,为了行文方便刻意区分了。
- 常见场景下的防御
注意:
由于公司以Java为主,因此如下场景均为Java相关,其他语言参考2. 防御思想 寻找方案(如python,搜索“python 预编译 防止SQL注入”)
通过2. 防御思想 ,我们容易发现预编译是一个通用可靠的方案,但是有些场景下可能不支持,我们需要通过打破“攻击者可控“来防御
3.1 无法使用预编译的场景
当SQL使用动态的表名、列名,那么就无法使用预编译了,因为预编译是需要绑定具体的表名和列名。
这时候怎么防御呢?我们需要通过打破“攻击者可控“来防御。比如检查表名是否在您定义的列表中(如"aa",“bb”,“cc”),从而保证输入是可信的。
安全部提供的公共SDK,提供了更加通用的防御方案:
Java SecSdk 防御SQL注入
Node.js SecSdk 防御SQL注入
其内部检查输入(单个字符)是否是有效的标志符,通过这一个限制保证攻击者无法注入有效的SQL,即攻击者不可控。
3.2 可以使用预编译的场景
3.2.1 原生的JDBC SQL
没有使用预编译:
Statement smt = conn.createStatement();
String sql = "SELECT id, name FROM `product` WHERE `name` LIKE '%" + keyword + "%'";
ResultSet rs = stmt.executeQuery(sql);
使用预编译:
String sql = "SELECT `id`, name FROM `product` WHERE `name` LIKE '%?%'";
PreparedStatement pSmt = conn.prepareStatement(sql);
pSmt.setString(1, keyword);
ResultSet rs = pStmt.execute();
使用预编译,保证用户传入的keyword只能是“数据”,而不会是“代码”。
3.2.2 mybatis
mybatis(ibatis)提供了良好的封装,#和$是区分动态参数是使用预编译还是拼接的重要特征。
因此,除了无法使用预编译的场景,其余场景使用“#”启用预编译就能免疫SQL注入了。至于无法使用预编译的场景,3.1中已经提供防御方法了。
针对能使用预编译的mybatis场景,我们举几个例子说明:
a.常规的条件
SELECT `id`, name FROM `product` WHERE `id` = #{id};
b.LIKE 条件(针对mysql,其他DBMS提供的函数不同略有差异)
SELECT `id`, name FROM `product` WHERE `name` = CONCAT('%',#{keyword},'%');
c.IN 条件
SELECT `id`, name FROM `product` WHERE `name` IN
<foreach item="item" index="index" collection="list" open="(" separator="," close=")">
#{item}
</foreach>
d.警惕 MyBatis generator Example中的SQL注入
MyBatis generator能大大降低编写SQL中的成本,其Example允许开发者在不同的业务场景下复用一个SQL,大部分情况下您可以放心使用MyBatis generator,但是仍需小心少量场景下的SQL注入。
如下是一个Example示例,order by ${orderByClause} 中是有SQL注入风险的,如果排序条件来自不可信输入且没有检查(如需检查,可参考上方Sdk),那么攻击者借此能注入SQL。
<sql id="Example_Where_Clause" >
<where >
<foreach collection="oredCriteria" item="criteria" separator="or" >
<if test="criteria.valid" >
<trim prefix="(" suffix=")" prefixOverrides="and" >
<foreach collection="criteria.criteria" item="criterion" >
<choose >
<when test="criterion.noValue" >
and ${criterion.condition}
</when>
<when test="criterion.singleValue" >
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue" >
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue" >
and ${criterion.condition}
<foreach collection="criterion.value" item="listItem" open="(" close=")" separator="," >
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
<select id="selectByExample" resultMap="BaseResultMap" parameterType="com.doctor.mybatisdemo.domain.userExample" >
select
<if test="distinct" >
distinct
</if>
<include refid="Base_Column_List" />
from user
<if test="_parameter != null" >
<include refid="Example_Where_Clause" />
</if>
<if test="orderByClause != null" >
order by ${orderByClause}
</if>
</select>
3.2.3 JdbcTemplate
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
上面可能没有把您使用的场景列举出来,但是只要掌握了思想,相信您能找到防御的手段。
- 思考
1)预编译是如何做到防止SQL注入的?
2)MyBatis generator Example示例中,为什么order by ${orderByClause} 中是有SQL注入风险?而其他条件没有?查看源代码找答案。