MySQL的allowMultiQueries标志与JDBC和jOOQ的关系

MySQL的JDBC连接器有一个安全特性,叫做 [allowMultiQueries](https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-security.html)``false当关闭时,它防止通过JDBC使用MySQL中一个有用但有潜在危险的功能。

try (Statement s = connection.createStatement()) {
    try {
        s.execute("create table t (i int);");

        // This doesn't work, by default:
        s.executeUpdate("""
            insert into t values (1);
            insert into t values (2);
        """);
    }
    finally {
        s.execute("drop table t");
    }
}

在默认情况下,上述内容会产生一个语法错误。

Exception in thread "main" java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'insert into t values (2)' at line 2
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.StatementImpl.executeUpdateInternal(StatementImpl.java:1333)
	at com.mysql.cj.jdbc.StatementImpl.executeLargeUpdate(StatementImpl.java:2106)
	at com.mysql.cj.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1243)
	at org.jooq.testscripts.JDBC.main(JDBC.java:34)

除非我们在JDBC连接URL中打开allowMultiQueries=true ,否则我们不能像这样连锁语句。

jdbc:mysql://localhost/test?allowMultiQueries=true

而现在,突然间,语句批处理正常完成了,两条记录被插入到表中。

为什么有这个功能?

这个安全功能有助于防止一些SQL注入漏洞。现在更难附加额外的语句了,万一你有串联值的坏主意,比如说,你的字符串。

// Terrible idea:
s.executeUpdate("insert into t values (" + value + ")");

因为,如果value 包含字符串"1); drop table t;" 呢?这在语法上是正确的,所以它将 "按预期 "执行。 这就不是很好了。

现在不要有一种错误的安全感。关掉这个功能并不能防止所有的SQL注入漏洞。只是让这个特定的漏洞变得更难。仍然有各种不同的方式,这种缺乏使用绑定变量的情况会导致攻击者读取你的数据,例如,通过基于时间的攻击

SQL注入的风险需要被认真对待。最好的办法是总是写带有绑定变量的静态SQL(例如:PreparedStatement 、存储过程或jOOQ),或者用jOOQ这样的SQL生成器来写动态SQL。

在jOOQ中使用allowMultiQueries

在使用jOOQ时,上述情况是很难发生的。jOOQ的默认用法是使用。

只有在极少数情况下,你才会使用普通的SQL模板来解决jOOQ中特定的功能不足问题,在这种情况下,模板语言会帮助你避免串联字符串和遇到SQL注入漏洞。

如果你是那种小心翼翼的人,你可以在你的构建中添加一个注释处理器,防止在jOOQ中使用普通的SQL API(任何使用都不会被默认编译,除非你明确选择)。

所以,MySQL标志对你的jOOQ使用并不真正有用。事实上,这甚至是一个问题,因为jOOQ内部依赖于生成如上所述的语句批处理。下面是一些当你关闭allowMultiQueries=false 时不能正常工作的功能(其中大部分也适用于MariaDB,btw)。

GROUP_CONCAT

每当你在MySQL的jOOQ中使用GROUP_CONCAT ,jOOQ就会假定你还没有改变MySQL的默认值为 [@@group_concat_max_length](https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_group_concat_max_len).该默认值是非常低的,即1024 。而这个值不仅阻止了较大数据集的字符串聚合,而且只是默默地失败,这就产生了错误的值!

当使用GROUP_CONCAT 在MySQL中模拟JSON_ARRAYAGG() ,在产生的JSON数组中通常会有一个可检测的语法错误,但当你只想产生一些字符串值,例如逗号分隔的列表时,情况就不是这样了。(参见之前的博客,为什么我们还不使用本地的JSON_ARRAYAGG() 支持)。

所以,每次你明确使用GROUP_CONCAT (或者jOOQ在内部使用它进行一些模拟)时,jOOQ会做的是预置和附加以下语句。

-- These are prepended
SET @t = @@group_concat_max_len;
SET @@group_concat_max_len = 4294967295;

-- Actual statement here:
SELECT group_concat(A SEPARATOR ',') FROM T;

-- These are appended
SET @@group_concat_max_len = @t;

如果你已经自己固定了系统或会话变量,你可以通过改变Settings.renderGroupConcatMaxLenSessionVariable 标志来关闭这个功能。

创建或替换函数

许多SQL方言对存储过程、函数、触发器和其他不包含数据的存储对象都有一个CREATE OR REPLACE 语法。这是非常有用的语法糖,用于编写这个,而不是。

-- Much simpler
CREATE OR REPLACE FUNCTION f ...

-- Than this
DROP FUNCTION IF EXISTS f;
CREATE FUNCTION f ...

但同样的,如果你关闭了allowMultiQueries=false ,那么jOOQ中的这种模拟就不能工作了,你又会得到一个语法错误。在这里,jOOQ不能为你做什么。你必须手动运行这两条语句,而不是使用方便的语法。

FOR UPDATE WAIT n

许多方言都有一个FOR UPDATE WAIT n 语法,允许为悲观的锁指定一个WAIT 超时,例如:

SELECT *
FROM t
FOR UPDATE WAIT n;

MySQL 8.0.26还不支持这个功能,但自从jOOQ 3.15和#11543以来,我们正在使用这个语法来模拟上述语法。

SET @t = @@innodb_lock_wait_timeout;
SET @@innodb_lock_wait_timeout = 2;
SELECT *
FROM t
FOR UPDATE;
SET @@innodb_lock_wait_timeout = @t;

另一件事是,如果你有以下情况就不能工作了allowMultiQueries=false

匿名块

许多过程性语言支持过程性代码的匿名块,即不存储在过程中的过程性代码。这是很有意义的。毕竟,我们也不必将所有的SQL存储在视图中,那么为什么我们要将PL/SQL、T-SQL、PL/pgSQL等存储在存储过程中呢?特别是当你想动态地生成这些块时,这可能非常有用,使用jOOQ在服务器上而不是在客户端运行一些逻辑,减少往返次数

在Oracle中,你可以写。

BEGIN
  INSERT INTO t VALUES (1);

  IF TRUE THEN
    INSERT INTO t VALUES (2);
  END IF;
END;

jOOQ从3.12开始支持这种匿名块。看看关于IF 语句的手册页面。你可以写

// Assuming the usual static imports:
import static org.jooq.impl.DSL.*;
import static org.jooq.impl.SQLDataType;

// Then write:
Variable<Integer> i = var("i", INTEGER);

ctx.begin(
  declare(i).set(1),

  if_(i.eq(0)).then(
    insertInto(A).columns(A.COL).values(1)
  ).elsif(i.eq(1)).then(
    insertInto(B).columns(B.COL).values(2)
  ).else_(
    insertInto(C).columns(C.COL).values(3)
  )
).execute();

这在那些支持匿名块的方言中翻译成并执行正确的过程性匿名块,但不幸的是,MySQL 8.0.26还不支持,那么我们该怎么做?我们生成一个 "匿名 "过程,调用它,然后再次放弃。

CREATE PROCEDURE block_1629705082441_2328258() 
BEGIN
DECLARE i INT; 
  SET i = 1; 

  IF i = 0 THEN
    INSERT INTO a (col) VALUES (1);
  ELSEIF i = 1 THEN
    INSERT INTO b (col) VALUES (2);
  ELSE
    INSERT INTO c (col) VALUES (3);
  END IF; 
END; 
CALL block_1629705082441_2328258(); 
DROP PROCEDURE block_1629705082441_2328258;

我的意思是,为什么不呢?但是,这又依赖于allowMultiQueries=true ,否则,JDBC驱动程序会拒绝这个语句。

关于jOOQ中过程性语言API的更多信息,请参考: https://blog.jooq.org/vendor-agnostic-dynamic-procedural-logic-with-jooq/

结论

MySQL的JDBC驱动程序有一个很好的安全功能,旨在防止一些SQL注入的情况,特别是当用户使用JDBC连接进行手动SQL执行时。团队中总有那么一个可怜的人还不知道SQL注入,因此弄错了,打开了潘多拉的盒子。对于这些用途,allowMultiQueries=false 是一个合理的默认值。

当按照jOOQ的意图使用jOOQ时,SQL注入的可能性要小得多这不包括纯SQL模板的使用,不过这篇文章并不适用。另一方面,jOOQ内部依靠allowMultiQueries=true ,以实现一些需要在一次往返中执行多个语句的模拟。

未来的jOOQ版本将允许配置多查询的执行模型,从而使上述情况可以作为多次往返执行。更多细节见#9645

在此之前,如果你想从jOOQ和MySQL中获得最大的好处,请确保在你的jOOQ连接上打开allowMultiQueries=true ,可能在其他地方保持关闭。

猜你喜欢

转载自juejin.im/post/7126364839420624933