网站首页 文章专栏 深入druid之sqlparse,druid如何使用sqlparse的
深入druid之sqlparse,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。

基本的拦截配置:

配置项缺省值描述
selelctAllowtrue是否允许执行SELECT语句
selectAllColumnAllowtrue是否允许执行SELECT * FROM T这样的语句。如果设置为false,不允许执行select * from t,但select * from (select id, name from t) a。这个选项是防御程序通过调用select *获得数据表的结构信息。
selectIntoAllowtrueSELECT查询中是否允许INTO字句
deleteAllowtrue是否允许执行DELETE语句
updateAllowtrue是否允许执行UPDATE语句
insertAllowtrue是否允许执行INSERT语句
replaceAllowtrue是否允许执行REPLACE语句
mergeAllowtrue是否允许执行MERGE语句,这个只在Oracle中有用
callAllowtrue是否允许通过jdbc的call语法调用存储过程
setAllowtrue是否允许使用SET语法
truncateAllowtruetruncate语句是危险,缺省打开,若需要自行关闭
createTableAllowtrue是否允许创建表
alterTableAllowtrue是否允许执行Alter Table语句
dropTableAllowtrue是否允许修改表
commentAllowfalse是否允许语句中存在注释,Oracle的用户不用担心,Wall能够识别hints和注释的区别
noneBaseStatementAllowfalse是否允许非以上基本语句的其他语句,缺省关闭,通过这个选项就能够屏蔽DDL。
multiStatementAllowfalse是否允许一次执行多条语句,缺省关闭
useAllowtrue是否允许执行mysql的use语句,缺省打开
describeAllowtrue是否允许执行mysql的describe语句,缺省打开
showAllowtrue是否允许执行mysql的show语句,缺省打开
commitAllowtrue是否允许执行commit操作
rollbackAllowtrue是否允许执行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的条件给忽略了,导致本来应该查询一条结果的,变成了查询全部。我们再跑下,结果测试不通过,说明代码符合预期。


1636645151(1).jpg


那么是怎么实现的呢,我们看下代码

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就被发现了。

还有这些默认的配置:

配置项缺省值描述
selectWhereAlwayTrueChecktrue检查SELECT语句的WHERE子句是否是一个永真条件
selectHavingAlwayTrueChecktrue检查SELECT语句的HAVING子句是否是一个永真条件
deleteWhereAlwayTrueChecktrue检查DELETE语句的WHERE子句是否是一个永真条件
deleteWhereNoneCheckfalse检查DELETE语句是否无where条件,这是有风险的,但不是SQL注入类型的风险
updateWhereAlayTrueChecktrue检查UPDATE语句的WHERE子句是否是一个永真条件
updateWhereNoneCheckfalse检查UPDATE语句是否无where条件,这是有风险的,但不是SQL注入类型的风险
conditionAndAlwayTrueAllowfalse检查查询条件WHERE,HAVING子句,中是否包含AND永真条件
conditionAndAlwayFalseAllowfalse检查查询条件WHERE,HAVING子句,中是否包含AND永假条件
conditionLikeTrueAllowtrue

检查查询条件WHERE,HAVING子句,中是否包含LIKE永真条件 


这里只是一个基本的语法检测逻辑,其他的基本也类似,将sql解析为语法树,再加载配置信息,通过visitor遍历语法,结合配置就发现了不合规的sql。也就完成了sql的注入检查。通过这个过程发现,sqlparse应用起来还是挺爽的,很方便的就对sql语法进行了检查,而且是基于语义的,也就是说我们可以随意根据我们的业务规则对sql进行审查,判断,改写。

肥~肠好用!其实我司在低代码领域对sqlparse也应用的很广泛,低代码实践起来就是定义一种与前端组件交互的通用的语言DSL,将这个语言解析后,通过对模型中sql进行分析改写,达到只用拖拉拽,可视化的布局,完成对sql数据的定制化改写。









版权声明:本文由星尘阁原创出品,转载请注明出处!

本文链接:http://www.52xingchen.cn/detail/92




赞助本站,网站的发展离不开你们的支持!
来说两句吧
大侠留个名吧,或者可以使用QQ登录。
: 您已登陆!可以继续留言。
最新评论