【问题标题】:How Do You Check For DELETE/UPDATE Without A WHERE Clause如何在没有 WHERE 子句的情况下检查 DELETE/UPDATE
【发布时间】:2017-10-25 16:02:00
【问题描述】:

我目前有一个侦听器,我们用它来执行一些不同的监视类型活动(例如,如果查询时间超过 5 秒,则记录警告),但它也监视并杀死“愚蠢的错误”——尤其是 @ 987654321@ 和 DELETE 查询缺少 WHERE 子句。

过去我们做了以下事情(注意我们使用的是com.foundationdb.sql):

/**
 * Hook into the query execution lifecycle before rendering queries. We are checking for silly mistakes,
 * pure SQL, etc.
 */
@Override
public void renderStart(final @NotNull ExecuteContext ctx) {
    if (ctx.type() != ExecuteType.WRITE)
        return;

    String queryString = ctx.sql();
    try (final Query query = ctx.query()) {

        // Is our Query object empty? If not, let's run through it
        if (!ValidationUtils.isEmpty(query)) {
            queryString = query.getSQL(ParamType.INLINED);

            final SQLParser parser = new SQLParser();
            try {
                final StatementNode tokens = parser.parseStatement(query.getSQL());
                final Method method = tokens.getClass().getDeclaredMethod("getStatementType");
                method.setAccessible(true);
                switch (((Integer) method.invoke(tokens)).intValue()) {                 
                    case StatementType.UPDATE:
                        SelectNode snode = ConversionUtils.as(SelectNode.class,
                                ((DMLStatementNode) tokens).getResultSetNode());

                        // check if we are a mass delete/update (which we don't allow)
                        if ((Objects.isNull(snode)) || (Objects.isNull(snode.getWhereClause())))
                            throw new RuntimeException("A mass update has been detected (and prevented): "
                                    + DatabaseManager.getBuilder().renderInlined(ctx.query()));
                        break;
                    case StatementType.DELETE:
                        snode = ConversionUtils.as(SelectNode.class,
                                ((DMLStatementNode) tokens).getResultSetNode());

                        // check if we are a mass delete/update (which we don't allow)
                        if ((Objects.isNull(snode)) || (Objects.isNull(snode.getWhereClause())))
                            throw new RuntimeException("A mass delete has been detected (and prevented): "
                                    + DatabaseManager.getBuilder().renderInlined(ctx.query()));
                        break;
                    default:
                        if (__logger.isDebugEnabled()) {
                            __logger
                                    .debug("Skipping query because we don't need to do anything with it :-): {}", queryString);
                        }
                }
            } catch (@NotNull StandardException | IllegalAccessException
                    | IllegalArgumentException | InvocationTargetException | NoSuchMethodException
                    | SecurityException e) {
                // logger.error(e.getMessage(), e);
            }
        }
        // If the query object is empty AND the SQL string is empty, there's something wrong
        else if (ValidationUtils.isEmpty(queryString)) {
            __logger.error(
                    "The ctx.sql and ctx.query.getSQL were empty");
        } else
            throw new RuntimeException(
                    "Someone is trying to send pure SQL queries... we don't allow that anymore (use jOOQ): "
                            + queryString);
    }
}

真的不想使用其他工具——尤其是因为大多数 SQL 解析器无法处理 UPSERTs 或 jOOQ 可以处理的各种查询,所以很多只是被删掉——并且很想使用jOOQ的构造,但我遇到了麻烦。理想情况下,我可以只检查查询类,如果它是更新或删除(或子类),如果它不是 UpdateConditionStep 或 DeleteConditionStep 的实例,我会尖叫,但这不起作用,因为查询返回为UpdateQueryImpl...而且没有疯狂的反思,我看不到有没有使用的条件。

所以……我现在正在做:

/**
 * Hook into the query execution lifecycle before rendering queries. We are checking for silly mistakes, pure SQL,
 * etc.
 */
@Override
public void renderStart(final @NotNull ExecuteContext ctx) {
    if (ctx.type() != ExecuteType.WRITE)
        return;

    try (final Query query = ctx.query()) {
        // Is our Query object empty? If not, let's run through it
        if (!ValidationUtils.isEmpty(query)) {
            // Get rid of nulls
            query.getParams().entrySet().stream().filter(entry -> Objects.nonNull(entry.getValue()))
                    .filter(entry -> CharSequence.class.isAssignableFrom(entry.getValue().getDataType().getType()))
                    .filter(entry -> NULL_CHARACTER.matcher((CharSequence) entry.getValue().getValue()).find())
                    .forEach(entry -> query.bind(entry.getKey(),
                            NULL_CHARACTER.matcher((CharSequence) entry.getValue().getValue()).replaceAll("")));

            if (Update.class.isInstance(query)) {
                if (!UpdateConditionStep.class.isInstance(query)) {
                    if (!WHERE_CLAUSE.matcher(query.getSQL(ParamType.INDEXED)).find()) {
                        final String queryString = query.getSQL(ParamType.INLINED);
                        throw new RuntimeException(
                                "Someone is trying to run an UPDATE query without a WHERE clause: " + queryString);
                    }
                }
            } else if (Delete.class.isInstance(query)) {
                if (!DeleteConditionStep.class.isInstance(query)) {
                    if (!WHERE_CLAUSE.matcher(query.getSQL(ParamType.INDEXED)).find()) {
                        final String queryString = query.getSQL(ParamType.INLINED);
                        throw new RuntimeException(
                                "Someone is trying to run a DELETE query without a WHERE clause: " + queryString);
                    }
                }
            }
        } else
            throw new RuntimeException(
                    "Someone is trying to send pure SQL queries... we don't allow that anymore (use jOOQ): "
                            + ctx.sql());
    }
}

这让我摆脱了第三方 SQL 解析器,但现在我在非内联查询上使用正则表达式来查找 \\s[wW][hH][eE][rR][eE]\\s,这也不理想。

  1. 有没有办法使用 jOOQ 告诉我 UPDATEDELETE 是否有 WHERE 子句?
  2. 同样,有没有一种方法可以让我查看查询所针对的表(以便我可以限制某人可以对其执行可变操作的表 - 显然不会检查它是 UPDATE 还是 @ 987654332@,而不是使用ExecuteType)?

【问题讨论】:

  • 听起来很酷。好奇,你在那里用的是什么SQLParser?我会尽快回复...
  • 我们正在使用 FoundationDB SQL 解析器 (com.foundationdb.sql)。没关系,但看起来很基本。并不是我责怪他们:-)

标签: java sql jooq


【解决方案1】:

这是一个有趣的想法和方法。我可以看到的一个问题是性能。第二次渲染 SQL 字符串然后再次解析它听起来有点开销。也许,这个ExecuteListener 应该只在开发和集成测试环境中有效,而不是在生产环境中。

关于您的问题

  1. 有没有办法使用 jOOQ 告诉我 UPDATE、DELETE 是否有 WHERE 子句?

由于您似乎愿意使用反射来访问第三方库的内部,当然,您可以检查ctx.query() 的类型是org.jooq.impl.UpdateQueryImpl 还是org.jooq.impl.DeleteQueryImpl。在 3.10.1 版本中,它们都有一个私人 condition 成员,您可以检查一下。

这显然会在内部发生任何变化时中断,但目前它可能是一个务实的解决方案。

  1. 同样,有没有办法让我看看查询针对的是哪个表

更通用和更健壮的方法是实现VisitListener,这是在表达式树遍历期间调用的 jOOQ 回调。您可以挂钩 SQL 字符串的生成和绑定变量的集合,并在遇到错误时立即抛出错误:

  • UPDATEDELETE 声明
  • ...没有WHERE 子句
  • ...从一组特定的表中更新一个表

你“只是”必须实现一个堆栈机器,在抛出异常之前记住所有上述内容。此处给出了如何实现VisitListener 的示例: https://blog.jooq.org/2015/06/17/implementing-client-side-row-level-security-with-jooq

未来的新功能

这种功能也已经在邮件列表中讨论过几次。 jOOQ 原生支持这是一个容易实现的目标。为此,我为 jOOQ 3.11 创建了一个功能请求: https://github.com/jOOQ/jOOQ/issues/6771

【讨论】:

  • 看,我同意旧方法的开销——让 jOOQ 创建 SQL 字符串,然后让解析器解析字符串,然后根据它做出决策,其中一些包括正则表达式, 有效,但它比我想要的要重得多。我真正想要的是能够防止某些邪恶的人在我们的公共系统中更新、删除等;防止“失控”,在所有系统中意外删除/更新。当然,理想情况下,测试会发现这些错误,但您知道不仅要获得 100% 的代码覆盖率,还要涵盖所有正面/负面测试是多么困难。
  • 3.11 功能请求会很棒。能够检查那种东西真是太好了。我将从您 2015 年的博客文章中阅读更多内容,看看它对 #2 的作用——感谢您的帮助!
  • @DanO: #6771 已经在 master 上实现,因此您可以将其反向移植到您的版本...The change wasn't too complex(并非全部都需要在反向移植中)。
猜你喜欢
  • 2011-12-07
  • 2012-05-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-05
  • 1970-01-01
  • 2012-05-15
相关资源
最近更新 更多