网站首页 文章专栏 深入druid之sqlparse,druid如何使用sqlparse的
MySql SQL全量统计
Hive/ODPS SQL执行安全审计
分库分表SQL解析引擎
数据库引擎的SQL Parser
防SQL注入
看得出其实功能很强大,既然拿到sql的语义了,那么基于语义就能做很多事,可以分析sql,对sql进行安全审计,改写sql达到分库分表的目的,也可以分析语义防注sql注入。功能太多,今天挑一个防止sql注入的功能看下是如何使用的。
SQL注入攻击是黑客对数据库进行攻击的常用手段,Druid连接池内置了WallFilter 提供防SQL注入功能,在不影响性能的同时防御SQL注入攻击。druid使用了基于语意的防SQL注入,通过SQL Parser,能够完整解析mysql、sql server、oracle、postgresql的语法,通过语意分析能够精确识别SQL注入攻击。
druid是通过WallFilter来进行防御的,Druid目前支持mysql和oracle、sql server,下一步会支持postgres。
基本的拦截配置:
配置项 | 缺省值 | 描述 |
---|---|---|
selelctAllow | true | 是否允许执行SELECT语句 |
selectAllColumnAllow | true | 是否允许执行SELECT * FROM T这样的语句。如果设置为false,不允许执行select * from t,但select * from (select id, name from t) a。这个选项是防御程序通过调用select *获得数据表的结构信息。 |
selectIntoAllow | true | SELECT查询中是否允许INTO字句 |
deleteAllow | true | 是否允许执行DELETE语句 |
updateAllow | true | 是否允许执行UPDATE语句 |
insertAllow | true | 是否允许执行INSERT语句 |
replaceAllow | true | 是否允许执行REPLACE语句 |
mergeAllow | true | 是否允许执行MERGE语句,这个只在Oracle中有用 |
callAllow | true | 是否允许通过jdbc的call语法调用存储过程 |
setAllow | true | 是否允许使用SET语法 |
truncateAllow | true | truncate语句是危险,缺省打开,若需要自行关闭 |
createTableAllow | true | 是否允许创建表 |
alterTableAllow | true | 是否允许执行Alter Table语句 |
dropTableAllow | true | 是否允许修改表 |
commentAllow | false | 是否允许语句中存在注释,Oracle的用户不用担心,Wall能够识别hints和注释的区别 |
noneBaseStatementAllow | false | 是否允许非以上基本语句的其他语句,缺省关闭,通过这个选项就能够屏蔽DDL。 |
multiStatementAllow | false | 是否允许一次执行多条语句,缺省关闭 |
useAllow | true | 是否允许执行mysql的use语句,缺省打开 |
describeAllow | true | 是否允许执行mysql的describe语句,缺省打开 |
showAllow | true | 是否允许执行mysql的show语句,缺省打开 |
commitAllow | true | 是否允许执行commit操作 |
rollbackAllow | true | 是否允许执行roll back操作 |
如果把selectIntoAllow、deleteAllow、updateAllow、insertAllow、mergeAllow都设置为false,这就是一个只读数据源了。 ## 拦截配置-永真条件
我们测试一个demo
public class WallFilterTest2 extends TestCase { private DruidDataSource dataSource; private WallFilter wallFilter; protected void setUp() throws Exception { dataSource = new DruidDataSource(); dataSource.setUrl("jdbc:h2:mem:wall_test;"); dataSource.setFilters("wall"); dataSource.setDbType(JdbcConstants.MARIADB); dataSource.init(); wallFilter = (WallFilter) dataSource.getProxyFilters().get(0); } protected void tearDown() throws Exception { dataSource.close(); } public void test_wallFilter() throws Exception { wallFilter.checkValid("SELECT * FROM `test` WHERE id = 1 "); Assert.assertEquals(0, wallFilter.getViolationCount()); } }
如上一个测试用例,检测这个sql是否合法,运行后测试通过,说明没问题,我们再用下面这个sql试下:
SELECT * FROM `test` WHERE id = 1 or 1=1
这个sql不难看出是个基本的注入,使用or 1=1,把前面 id=1的条件给忽略了,导致本来应该查询一条结果的,变成了查询全部。我们再跑下,结果测试不通过,说明代码符合预期。
那么是怎么实现的呢,我们看下代码
checkValid(String sql)
该方法在WallFilter中定义,该类有一个provider属性,通过provider对sql进行检测
private WallProvider provider;
在初始化该类时,会通过dbType进行不同数据源类型的provider初始化
switch (dbType) { case mysql: case oceanbase: case drds: case mariadb: case h2: case presto: case trino: if (config == null) { config = new WallConfig(MySqlWallProvider.DEFAULT_CONFIG_DIR); } provider = new MySqlWallProvider(config); break; case oracle: ... }
如上加载一个配置类,并初始化MySqlWallProvider,这个MySqlWallProvider.DEFAULT_CONFIG_DIR是一个写死的路径"META-INF/druid/wall/mysql",是通过spi的形式动态加载配置。
那么我们就可以看下MySqlWallProvider中的checkValid方法就行。
MySqlWallProvider继承自WallProvider,在WallProvider 中有个checkValid的方法,就是在这执行的
public boolean checkValid(String sql) { WallContext originalContext = WallContext.current(); try { WallContext.create(dbType); WallCheckResult result = checkInternal(sql); return result .getViolations() .isEmpty(); } finally { if (originalContext == null) { WallContext.clearContext(); } } }
关键函数:checkInternal(String sql),这个方法很长,大概逻辑就是,先检查是否在白名单,如果是那就不检查了,如果不是则继续,然后创建一个SQLStatementParser进行解析
SQLStatementParser parser = createParser(sql); parser.getLexer().setCommentHandler(WallCommentHandler.instance); if (!config.isCommentAllow()) { parser.getLexer().setAllowComment(false); // deny comment } if (!config.isCompleteInsertValuesCheck()) { parser.setParseCompleteValues(false); parser.setParseValuesSize(config.getInsertValuesCheckSize()); } // 开始解析 parser.parseStatementList(statementList);
在解析的时候就对语法进行检测,如果不正确,就抛出异常,然后判断词尾是否是EOF,不是的话算作出现一例不合规,加入不合格集合中。
if (lastToken != Token.EOF && config.isStrictSyntaxCheck()) { violations.add(new IllegalSQLObjectViolation(ErrorCode.SYNTAX_ERROR, "not terminal sql, token " + lastToken, sql)); }
接着创建一个WallVisitor,实际类型是MySqlWallVisitor,然后对语法树进行遍历
WallVisitor visitor = createWallVisitor();
在MySqlWallVisitor中,再次进行调用
@Override public boolean visit(MySqlSelectQueryBlock x) { WallVisitorUtils.checkSelelct(this, x); return true; }
然后就是根据用户的配置,判断是否允许查询,是否允许有哪些关键字之类的,就是上面表格中配置的那些信息。然后检查where属性,拿到where条件 id = 1 or 1=1,通过语法树的语义分析得出结论,该where条件永为true,然后再通过配置中的一个属性conditionAndAlwayTrueAllow,判断是否算作sql注入。
conditionAndAlwayTrueAllow 意思就是允许where条件中包含永真条件,默认为 false,就是不允许,用来检查查询条件(WHERE/HAVING子句)中是否包含AND永真条件,正好我们这个是一个永真条件
if (current.hasPartAlwayTrue() && !visitor.getConfig().isConditionAndAlwayTrueAllow()) { addViolation(visitor, ErrorCode.ALWAYS_TRUE, "part alway true condition not allow", x); } Assert.assertEquals(0, wallFilter.getViolationCount());
所以添加到违反语法集合中,在结尾断言的时候,这个违反集合结果大于0,所以这条sql就被发现了。
还有这些默认的配置:
配置项 | 缺省值 | 描述 |
---|---|---|
selectWhereAlwayTrueCheck | true | 检查SELECT语句的WHERE子句是否是一个永真条件 |
selectHavingAlwayTrueCheck | true | 检查SELECT语句的HAVING子句是否是一个永真条件 |
deleteWhereAlwayTrueCheck | true | 检查DELETE语句的WHERE子句是否是一个永真条件 |
deleteWhereNoneCheck | false | 检查DELETE语句是否无where条件,这是有风险的,但不是SQL注入类型的风险 |
updateWhereAlayTrueCheck | true | 检查UPDATE语句的WHERE子句是否是一个永真条件 |
updateWhereNoneCheck | false | 检查UPDATE语句是否无where条件,这是有风险的,但不是SQL注入类型的风险 |
conditionAndAlwayTrueAllow | false | 检查查询条件WHERE,HAVING子句,中是否包含AND永真条件 |
conditionAndAlwayFalseAllow | false | 检查查询条件WHERE,HAVING子句,中是否包含AND永假条件 |
conditionLikeTrueAllow | true | 检查查询条件WHERE,HAVING子句,中是否包含LIKE永真条件 |
这里只是一个基本的语法检测逻辑,其他的基本也类似,将sql解析为语法树,再加载配置信息,通过visitor遍历语法,结合配置就发现了不合规的sql。也就完成了sql的注入检查。通过这个过程发现,sqlparse应用起来还是挺爽的,很方便的就对sql语法进行了检查,而且是基于语义的,也就是说我们可以随意根据我们的业务规则对sql进行审查,判断,改写。
肥~肠好用!其实我司在低代码领域对sqlparse也应用的很广泛,低代码实践起来就是定义一种与前端组件交互的通用的语言DSL,将这个语言解析后,通过对模型中sql进行分析改写,达到只用拖拉拽,可视化的布局,完成对sql数据的定制化改写。
版权声明:本文由星尘阁原创出品,转载请注明出处!