首页 / 帮助文档 / 防止SQL注入的Druid连接池SQL解析与拦截

防止SQL注入的Druid连接池SQL解析与拦截

SQL注入攻击至今仍是Web安全领域最普遍的威胁之一,即便在ORM框架广泛使用的今天,仍有大量遗留系统或复杂查询场景直接拼接SQL。Druid连接池内置的WallFilter提供了一套不依赖业务代码改造的SQL防火墙机制,它通过对SQL语句进行词法分析、语法解析和语义检查,在数据库驱动层面就拦截掉恶意注入。这套机制的核心在于Druid自己实现的SQL解析器,它能把一条SQL字符串拆解成抽象语法树,然后遍历这棵树去判断是否存在危险操作。

很多人以为只要用了PreparedStatement就万事大吉,但实际上ORDER BY、表名、列名这类动态拼接场景根本无法参数化,而这些位置恰恰是注入的高发区。Druid的WallFilter针对的正是这类“参数化无法覆盖”的场景,它直接分析整条SQL的语义,不管参数从哪里来,只要最终拼出来的SQL语句存在异常结构,就能被识别出来。

Druid SQL解析器的核心架构

Druid的SQL解析模块位于com.alibaba.druid.sql包下,它支持主流数据库的SQL方言,包括MySQL、Oracle、PostgreSQL、SQL Server等。解析过程分为两个阶段:词法分析和语法分析。词法分析器把SQL字符串切分成一个个Token,比如SELECT、FROM、WHERE这些关键字,表名、列名这些标识符,以及数字、字符串等字面量。语法分析器则根据Token序列构建AST(抽象语法树),树的每个节点对应SQL语句的一个组成部分。

以一条简单的查询为例:

SELECT id, username FROM users WHERE id = 1

解析后生成的AST大致结构是:根节点是SQLSelectStatement,下面包含SQLSelect对象,SQLSelect里又包含SQLSelectQueryBlock,这个Block里分别有selectList(id和username两个列)、from子句(users表)和where条件(id = 1这个二元表达式)。每个节点都有明确的类型,程序可以精确地遍历和检查。

这套解析器的厉害之处在于它是完整实现的,不是简单的正则匹配。正则方案很容易被编码绕过,比如攻击者把关键字写成大小写混合、插入注释、使用十六进制编码等。而Druid的解析器严格按照各数据库的SQL规范实现,能正确识别各种等价写法,从根本上杜绝了绕过问题。

WallFilter的拦截机制详解

WallFilter在Druid中是以Filter链的形式工作的。当应用通过Druid连接池执行SQL时,WallFilter会在SQL被发送到数据库之前进行拦截检查。检查的核心逻辑在WallVisitor中实现,它遍历解析后的AST,对每个节点执行预定义的检查规则。

WallFilter的检查分为几个层次。第一层是黑白名单检查,Druid内置了一张允许执行的操作清单,比如普通的SELECT、INSERT、UPDATE、DELETE是允许的,但ALTER TABLE、DROP TABLE、TRUNCATE这类DDL操作默认被禁止。第二层是语义检查,比如检测是否存在UNION查询(常用于联合注入)、是否包含多语句(用分号分隔多条SQL)、是否访问了系统表等。第三层是函数检查,很多注入攻击会调用数据库的内置函数来获取信息,比如MySQL的load_file()、benchmark(),这些危险函数会被识别并拦截。

具体到代码层面,WallFilter的核心配置通过WallConfig类来控制。以下是一个典型的配置示例:

WallConfig wallConfig = new WallConfig();
// 允许执行UPDATE操作
wallConfig.setUpdateAllow(true);
// 禁止执行DELETE操作(无WHERE子句的DELETE)
wallConfig.setDeleteWhereNoneCheck(true);
// 禁止多语句执行
wallConfig.setMultiStatementAllow(false);
// 设置UNION查询的检查
wallConfig.setSelectUnionCheck(true);
// 禁止读取系统表
wallConfig.setSelectAllColumnAllow(false);
// 设置注释检查(防止注释绕过)
wallConfig.setCommentAllow(false);

这些配置项非常细致,可以根据业务需求灵活调整。比如有些BI系统确实需要执行UNION查询,那就单独放通;有些后台管理系统需要执行DDL,也可以针对特定数据源开放权限。

针对常见注入手法的拦截分析

联合查询注入是最经典的攻击方式,攻击者通过在正常查询后拼接UNION SELECT来窃取其他表的数据。WallFilter检测到UNION关键字时,会检查UNION前后的列数是否一致、是否访问了非授权表。即使攻击者使用注释符把后面的条件注释掉,解析器也能完整还原出SQL的真实语义。

布尔盲注和延时注入依赖条件判断函数。以MySQL为例,攻击者常用if(condition, 1, 0)或者sleep()函数来判断条件真假。WallFilter内置了危险函数黑名单,包含sleep、benchmark、load_file、into outfile等。当AST遍历到函数调用节点时,会检查函数名是否在黑名单中。这个黑名单是可以扩展的,通过wallConfig.getDenyFunctions().add("自定义危险函数")来添加。

堆叠注入利用分号执行多条SQL语句,比如在正常查询后拼接; DROP TABLE users--。WallFilter默认禁止多语句执行,一旦解析器发现SQL中包含多个完整的语句,就会直接拒绝。这个检查在SQLParser层面就完成了,解析结果中如果有多个Statement对象,就会被拦截。

还有一种容易被忽视的注入场景是ORDER BY和GROUP BY后的注入。这些位置通常无法使用参数化查询,攻击者可能传入类似id; DROP TABLE users--的值。Druid的解析器会正确解析ORDER BY后的表达式,如果发现它不是一个合法的列引用而是一个完整的子语句,就会被WallFilter识别为异常。

实战配置与调优建议

在生产环境中部署WallFilter时,不建议直接使用默认配置,而是要根据业务特点进行定制。首先需要开启WallFilter的统计功能,通过监控日志了解当前系统执行的SQL类型分布。Druid提供了WallStat来统计拦截情况,包括拦截次数、拦截类型、拦截的SQL样本等。

在Spring Boot项目中配置Druid的WallFilter非常简便,通常在application.yml中配置即可:

spring:
  datasource:
    druid:
      filters: wall,stat
      filter:
        wall:
          enabled: true
          config:
            select-all-column-allow: false
            delete-allow: true
            drop-table-allow: false
            multi-statement-allow: false

对于使用代码方式配置的场景,可以更精细地控制规则。比如针对某些特定表设置白名单,只允许对这些表执行增删改查操作:

WallConfig wallConfig = new WallConfig();
// 设置表名白名单
wallConfig.getTableCheck().add("users");
wallConfig.getTableCheck().add("orders");
wallConfig.getTableCheck().add("products");
// 其他表一律禁止访问
wallConfig.setTableCheck(true);

这个表级白名单功能非常实用,即使攻击者成功注入了SQL,只要目标表不在白名单中,查询依然会被拦截。这是一种纵深防御的思路,不依赖单一的拦截手段。

需要注意的是,WallFilter的解析和检查会消耗一定的CPU资源。对于高并发的OLTP系统,这部分开销通常可以忽略不计,因为Druid的解析器经过大量优化,性能表现优秀。但如果系统中有大量复杂SQL(比如包含几十个JOIN的报表查询),建议在测试环境评估性能影响。可以通过WallFilter的统计监控观察解析耗时,如果确实存在瓶颈,可以考虑对这部分SQL做业务层面的优化,减少不必要的复杂度。

绕过技术与防御演进

任何安全机制都需要持续对抗绕过技术。Druid WallFilter历史上也经历过多次绕过漏洞的修复,这些案例能帮助我们理解防御的边界。常见的绕过思路包括利用数据库特性差异、编码转换、注释插入等。

一种典型的绕过手法是利用MySQL的条件注释。MySQL支持/!50000 SELECT .../这种格式的条件注释,当数据库版本大于等于指定版本时,注释中的内容会被执行。早期版本的Druid解析器可能忽略了这种注释中的SQL,导致绕过。后续版本已经修复了这个问题,解析器会递归处理条件注释中的内容。

另一种绕过思路是利用不同数据库对字符串处理的差异。比如在MySQL中,字符串可以用单引号、双引号、反引号甚至十六进制表示。如果解析器对某一种表示方式处理不完善,攻击者就可能利用它来隐藏恶意代码。Druid通过实现完整的词法分析器来处理各种字符串表示,确保无论使用哪种写法,都能被正确解析为字符串字面量。

还有攻击者尝试利用SQL语句的等价变形来绕过语义检查。比如UNION SELECT可以写成UNION ALL SELECT,或者利用子查询嵌套来隐藏真实意图。WallFilter的检查是基于AST结构的,不管外层如何变形,只要最终生成的AST中包含危险结构,就能被识别。这种基于语义的检测方式比基于特征的匹配要健壮得多。

保持Druid版本更新是应对绕过攻击最有效的手段。每个新版本都会修复已知的解析缺陷和安全漏洞,同时增强对新型攻击手法的检测能力。建议在生产环境中使用最新稳定版本,并关注官方发布的安全公告。

与其他防护手段的协同

WallFilter不是SQL注入防护的银弹,它应该作为纵深防御体系中的一层来使用。在应用层,仍然要坚持使用参数化查询,这是防止注入最根本的手段。WallFilter主要覆盖参数化无法解决的场景,以及作为参数化查询失效时的兜底防护。

在Web应用防火墙层面,也可以配置SQL注入检测规则,与WallFilter形成互补。WAF擅长检测HTTP请求中的攻击载荷,而WallFilter在数据库驱动层面检测最终的SQL语句,两者检测的位置不同,能互相弥补盲区。比如攻击者可能通过二次注入的方式,先将恶意代码存入数据库,然后在后续查询中触发,这种情况下WAF可能看不到明显的攻击特征,但WallFilter能检测到最终执行的异常SQL。

数据库本身的权限控制也不能忽视。应用连接数据库的账号应该遵循最小权限原则,只授予必要的SELECT、INSERT、UPDATE、DELETE权限,不授予DDL权限和文件操作权限。这样即使WallFilter被绕过,攻击者能造成的破坏也受到限制。WallFilter中也可以配置类似的权限策略,与应用层的权限控制形成双重保障。

对于使用了Druid连接池的项目,开启WallFilter几乎是零成本的防护措施。它不需要修改业务代码,不需要调整SQL写法,只需要在配置文件中增加几行配置即可生效。这种低侵入性使得它非常适合在遗留系统中快速部署,为那些无法立即重构的SQL拼接代码提供保护。在安全防护领域,这种“不依赖业务改造”的方案往往是最容易落地、也最容易被团队接受的。