sql注入预编译无效的情况
错误的预编译使用
有的开发人员可能以为只要 sql
语句经过了预编译就不会造成 sql
注入。其实预编译加参数占位才可以防御 sql
注入,只要用了字符串拼接 sql
语句,就都会有 sql
注入的风险。下面是一个错误的示例。
1 | package com.just.demo; |
参数位于like位置
如果我们没有试过把参数放到 sql
语句中 like
的位置,我们第一想法可能 sql
语句会写成下面的样子:
1 | String sql = "select * from tb_user where username like '%?%'" ; |
但是可以发现上面的写法会无法绑定参数到 ?
的位置。这是因为 ?
的位置在两个引号中,此时程序会把这个 ?
当作字符串处理。
所以这种情况下,开发可能会选择直接使用拼接语句,如下:
1 | String sql = "select * from tb_user where username like '%" + var + "%'" ; |
可能开发以为这里使用了预编译就不会存在 sql
注入,但是这里由于用的是字符串拼接的 sql
语句,因此还是可以注入。
这里如果既想使用预编译,又想使用 like
模糊查询,正确的写法应该如下:
1 | String sql = "select * from tb_user where username like concat('%', ?, '%')" ; |
参数位于order by位置
这种情况我们第一想法的写法可能如下:
但是会发现这里 sql
语句执行的结果和预计的结果不一致。
这里因为预编译在参数绑定的时候会把参数当作字符串来处理,而 order by
后面不应该接字符串,否则 order by
和没用一样。
所以实际情况如果真的要 order by
后面接动态参数,这里大概率也会被写成拼接,从而造成 sql
注入。解决方式只能是对这里对传入的动态参数加内容过滤。
其它类似order by等不可参数化的地方
由于参数化会把参数都当作字符串来处理,而 sql
语句中有些地方不能是字符串,例如这里提到的 order by
,还有 group by
。
参数位于in位置
Mysql
的 jdbc
虽然对预编译提供了 setArray
() 和 conn.createArrayOf()
方法,但是并没有实现 conn.createArrayOf()
方法,导致如果想要 in
后面传入动态参数,大概率也要手动拼接传入数组内的元素,这就也可能造成 sql
注入。
可以看到这个方法压根没实现。
宽字节注入+假预编译
php
中的 pdo
默认使用的是 假预编译 来防止的 sql
注入。 真预编译 是对 sql
执行的过程进行预编译来参数绑定,而 假预编译 只是对 sql
语句做了转义。一个是数据库来实现的预编译,一个是程序框架来实现的预编译。
那为什么开发者要做一个虚假的预编译呢,那是因为一个参数:PDO::ATTR_EMULATE_PREPARES
,这个选项用来配置 PDO
是否使用模拟预编译,默认是 true
,因此默认情况下 PDO
采用的是模拟预编译模式,设置成 false
以后,才会使用真正的预编译。开启这个选项主要是用来兼容部分不支持预编译的数据库(如 sqllite
与低版本 MySQL
),对于模拟预编译,会 由客户端程序内部参数绑定这一过程(而不是数据库),内部 prepare
之后再将拼接的 sql
语句发给数据库执行 。
那么了解过宽字节注入的人就可以发现,假预编译是防不了宽字节注入的。
1 |
|
成功注入!
如果要使用真预编译,需要加上下面这一句代码。
1 | $conn->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); |
判断真假预编译的方式
最直观的判断方式就是看数据库的 sql
语句执行日志中是否执行了 prepare
操作。下面是一个真预编译情况:
而假预编译这里只会执行 execute
操作,而避免 sql
注入用的是转义或者其它操作,这就存在绕过的机会。
通过 \
来转义避免:
通过在 '
后面自动再加上一个 '
闭合参数中的引号来避免:
而实际测试发现,其实很多框架都默认使用的是假预编译,除非专门配置了预编译配置。
参考文章
https://forum.butian.net/share/1559
https://fushuling.com/index.php/2023/10/27/%E9%A2%84%E7%BC%96%E8%AF%91%E4%B8%8Esql%E6%B3%A8%E5%85%A5/
https://xz.aliyun.com/t/7132