MySQL JDBC Driver字符串占位符实现

2022年 4月 1日 70点热度 0人点赞

跟踪了一下 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 转义? 这个留作思考题, 动手试一下就知道为什么了.

FROM: https://www.cnblogs.com/iyangyuan/p/4809494.html

rainbow

这个人很懒,什么都没留下

文章评论