跟踪了一下 MySQL JDCB 驱动的 setString
方法, 方法的实现是在: com.mysql.cj.ClientPreparedQueryBindings#setString
.
驱动包: mysql-connector-java-8.0.21.jar
.
@Override
public void setString(int parameterIndex, String x) {
if (x == null) {
setNull(parameterIndex);
} else {
int stringLength = x.length();
// 省略中间代码
if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
needsQuoted = false; // saves an allocation later
StringBuilder buf = new StringBuilder((int) (x.length() * 1.1));
// 字符串前面加单引号
buf.append('\'');
for (int i = 0; i < stringLength; ++i) {
char c = x.charAt(i);
switch (c) {
// 省略中间处理
}
}
// 字符串最后再加单引号
buf.append('\'');
parameterAsString = buf.toString();
}
// 将字符串转为字节
byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(parameterAsString)
: (needsQuoted ? StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charEncoding)
: StringUtils.getBytes(parameterAsString, this.charEncoding));
setValue(parameterIndex, parameterAsBytes, MysqlType.VARCHAR);
}
}
PreparedStatement 会为占位符 ?
的两边自动加上单引号, 这样会使得 SQL 语句不可执行, 比如使用将表名设置为占位符, 数据库执行 sql 语句时, 表名会用单引号引起来, 这样会使得 SQL 语句执行出错或者查询不出数据.
SELECT SUM('sellnumbers') a, SUM('sellmoney') b FROM sell;
这句代码在 SQL 中执行不会报错, 但是查询的结果全部都是 0.
所以 PreparedStatement 只能用来为可以加引号的参数 (如参数值) 设置动态参数, 即用 ?
占位, 不可用于表名, 字段名等, 不然怎么生成预编译的语句对象呢. 所以 SQL 中你必须知道你先要查询或操作那些字段.
同理, 下面的这个 SQL 语句也是有问题的, 如果第一个参数和第二个参数不相等则查询出来的记录数为 0, 如果相等就是返回表的所有记录.
select * from user where ? = ?
为什么 PreparedStatement 能防注入?
之所以 PreparedStatement 能防止注入, 是因为它把单引号转义了, 变成了 \'
, 这样一来, 就无法截断 SQL 语句, 进而无法拼接 SQL 语句, 基本上没有办法注入了.
如果不用 PreparedStatement, 又想防止注入, 最简单粗暴的办法就是过滤单引号, 过滤之后, 单纯从 SQL 的角度, 无法进行任何注入.
大多数注入, 还是发生在数值类型上, 幸运的是 PreparedStatement 为我们提供了 st.setInt(1, 999);这种数值参数赋值 API, 基本就避免了注入, 因为如果用户输入的不是数值类型, 类型转换的时候就报错了.
like 类型的查询
尝试输入了一个百分号, 发现 PreparedStatement 竟然没有转义, 百分号恰好是 like 查询的通配符.
正常情况下,like 查询是这么写的:
String sql = "select * from goods where min_name like ?"; // 含有
st = conn.prepareStatement(sql);
st.setString(1, "儿童" + "%"); // 参数赋值
System.out.println(st.toString());
// com.mysql.jdbc.JDBC4PreparedStatement@8543aa: select * from goods where min_name like '儿童%'
- 直接拼接 SQL 语句, 然后自己实现所有的转义操作. 这种方法比较麻烦, 而且很可能没有 PreparedStatement 做的好, 造成其他更大的漏洞, 不推荐.
- 直接简单暴力的过滤掉%. 笔者觉得这方案不错, 如果没有严格的限制, 随便用户怎么输入, 既然有限制了, 就干脆严格一些, 干脆不让用户搜索%, 推荐.
为什么我们不能手动转义一下用户输入的%, 其他的再交给 PreparedStatement 转义? 这个留作思考题, 动手试一下就知道为什么了.
文章评论