jsqlparser:实现基于SQL语法分析的SQL注入攻击检查

Posted 10km

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jsqlparser:实现基于SQL语法分析的SQL注入攻击检查相关的知识,希望对你有一定的参考价值。

之前写过一篇博客:《java:正则表达式检查SQL WHERE条件语句防止注入攻击和常量表达式》,当前时通过正则表达式来检查SQL语句中是否有危险关键字和常量表达式实现SQL语句的注入攻击检查。坦率的说,这个办法是有漏洞的,误判,漏判的概率很大,基于当前我的知识能力,也只能做到这样。
最近学习了jsqlparser,我知道我找到了更好的办法来解决SQL注入攻击检查问题。
jsqlparser是一个java的SQL语句解析器,在上一篇博客:《jsqlparser:基于抽象语法树(AST)遍历SQL语句的语法元素》介绍了如何通过jsqlparser来遍历SQL语句中所有的字段和表名引用。
其实它可以用来进行更复杂的工作,jsqlparser会将一条SQL语句的各种语法元素以抽象语法树(AST,abstract syntax tree)形式解析为很多不同类型对象,通过对AST的遍历就可以对SQL语句进行分析。采用这种方式做SQL注入攻击检查不会有误判,漏判的问题。

SqlInjectionAnalyzer

SqlInjectionAnalyzer SQL注入攻击分析器
jsqlparse提供了两种式遍历SQL语句解析对象,一种是基于TablesNamesFinder,TablesNamesFinder其实是实现jsqparser很多对象访问接口的一个基类
一种是基于CCJSqlParserDefaultVisitor接口。

/**
 * SQL注入攻击分析器
 * @author guyadong
 *
 */
public class SqlInjectionAnalyzer 
    private boolean injectCheckEnable = true;
    private final InjectionSyntaxObjectAnalyzer injectionChecker;
    private final InjectionAstNodeVisitor injectionVisitor;
    public SqlInjectionAnalyzer() 
        this.injectionChecker = new InjectionSyntaxObjectAnalyzer(); 
        this.injectionVisitor = new InjectionAstNodeVisitor();
    
    /**
     * 启用/关闭注入攻击检查,默认启动
     * @param enable
     * @return
     */
    public SqlInjectionAnalyzer injectCheckEnable(boolean enable)
        injectCheckEnable = enable;
        return this;
    
    /**
     * 对解析后的SQL对象执行注入攻击分析,有注入攻击的危险则抛出异常@link InjectionAttackException
     * @param sqlParserInfo
     * @throws InjectionAttackException 
     */
    public SqlParserInfo injectAnalyse(SqlParserInfo sqlParserInfo) throws InjectionAttackException

        if(null != sqlParserInfo && injectCheckEnable)
            /** SQL注入攻击检查 */
            sqlParserInfo.statement.accept(injectionChecker);
            sqlParserInfo.simpleNode.jjtAccept(injectionVisitor, null);             
        
        return sqlParserInfo;
    


InjectionSyntaxObjectAnalyzer

InjectionSyntaxObjectAnalyzer 为基于SQL语法对象的SQL注入攻击分析实现
TablesNamesFinder是jsqlparser提供的一个语法元素遍历对象,继承这个对象可以实现对需要的语法元素的访问,当遇到有注入攻击危险的表达式,语句时抛出InjectionAttackException异常,就是这个类做的工作


import net.sf.jsqlparser.expression.BinaryExpression;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.ComparisonOperator;
import net.sf.jsqlparser.schema.Column;
import net.sf.jsqlparser.statement.select.Join;
import net.sf.jsqlparser.statement.select.OrderByElement;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.SelectItem;
import net.sf.jsqlparser.statement.select.SubSelect;
import net.sf.jsqlparser.statement.select.WithItem;
import net.sf.jsqlparser.util.TablesNamesFinder;

/**
 * 基于SQL语法对象的SQL注入攻击分析实现
 * @author guyadong
 *
 */
public class InjectionSyntaxObjectAnalyzer  extends TablesNamesFinder
    /** 危险函数名 */
	private static final String DANGROUS_FUNCTIONS = "(sleep|benchmark|extractvalue|updatexml|ST_LatFromGeoHash|ST_LongFromGeoHash|GTID_SUBSET|GTID_SUBTRACT|floor|ST_Pointfromgeohash"
			+ "|geometrycollection|multipoint|polygon|multipolygon|linestring|multilinestring)";
	
	private static ThreadLocal<Boolean> disableSubselect = new ThreadLocal<Boolean>()
        @Override
        protected Boolean initialValue() 
            return true;
        ;
	private ConstAnalyzer constAnalyzer = new ConstAnalyzer();
    public InjectionSyntaxObjectAnalyzer() 
		super();
		init(true);
		
	
	@Override
    public void visitBinaryExpression(BinaryExpression binaryExpression) 
	    if(binaryExpression instanceof ComparisonOperator)
	        if(isConst(binaryExpression.getLeftExpression()) && isConst(binaryExpression.getRightExpression()))
	            /** 禁用恒等式 */
	            throw new InjectionAttackException("DISABLE IDENTICAL EQUATION " + binaryExpression);
	        
	    
	    super.visitBinaryExpression(binaryExpression);
    

    @Override
    public void visit(AndExpression andExpression) 
        super.visit(andExpression);
        checkConstExpress(andExpression.getLeftExpression());
        checkConstExpress(andExpression.getRightExpression());
    
    @Override
    public void visit(OrExpression orExpression) 
        super.visit(orExpression);
        checkConstExpress(orExpression.getLeftExpression());
        checkConstExpress(orExpression.getRightExpression());
    
    @Override
    public void visit(Function function) 
    	if(function.getName().matches(DANGROUS_FUNCTIONS))
    		/** 禁用危险函数 */
    		throw new InjectionAttackException("DANGROUS FUNCTION: "+function.getName());
    	
    	super.visit(function);
    
    @Override
    public void visit(WithItem withItem) 
        try 
            /** 允许 WITH 语句中的子查询 */
            disableSubselect.set(false);
            super.visit(withItem);
         finally 
            disableSubselect.set(true);
        
    
    @Override
    public void visit(SubSelect subSelect) 
        if(disableSubselect.get())
        	// 禁用子查询
            throw new InjectionAttackException("DISABLE subselect " + subSelect);
        
    
    @Override
    public void visit(Column tableColumn) 
        if(ParserSupport.isBoolean(tableColumn))
            throw new InjectionAttackException("DISABLE CONST BOOL " + tableColumn);
        
        super.visit(tableColumn);
    
    @Override
    public void visit(PlainSelect plainSelect) 
        if (plainSelect.getSelectItems() != null) 
            for (SelectItem item : plainSelect.getSelectItems()) 
                item.accept(this);
            
        

        if (plainSelect.getFromItem() != null) 
            plainSelect.getFromItem().accept(this);
        

        if (plainSelect.getJoins() != null) 
            for (Join join : plainSelect.getJoins()) 
                join.getRightItem().accept(this);
                for(Expression e:join.getOnExpressions())
                    e.accept(this);
                
            
        
        if (plainSelect.getWhere() != null) 
            plainSelect.getWhere().accept(this);
            checkConstExpress(plainSelect.getWhere());
        

        if (plainSelect.getHaving() != null) 
            plainSelect.getHaving().accept(this);
        

        if (plainSelect.getOracleHierarchical() != null) 
            plainSelect.getOracleHierarchical().accept(this);
        
        if(plainSelect.getOrderByElements() != null)
            for( OrderByElement orderByElement : plainSelect.getOrderByElements())
                orderByElement.getExpression().accept(this);
            
        
        if(plainSelect.getGroupBy() != null)
            for(Expression expression : plainSelect.getGroupBy().getGroupByExpressionList().getExpressions())
                expression.accept(this);
            
        
    

    private boolean isConst(Expression expression)
        return constAnalyzer.isConstExpression(expression);
    
    private void checkConstExpress(Expression expression)
        if(constAnalyzer.isConstExpression(expression))
            /** 禁用常量表达式 */
            throw new InjectionAttackException("DISABLE CONST EXPRESSION " + expression);
        
    


InjectionAstNodeVisitor

InjectionAstNodeVisitor 为基于抽象语法树(AST)的注入攻击分析实现
这部分代码很少,实现禁用用UNION语句

import net.sf.jsqlparser.parser.CCJSqlParserDefaultVisitor;
import net.sf.jsqlparser.parser.SimpleNode;
import net.sf.jsqlparser.statement.select.UnionOp;

/**
 * 基于抽象语法树(AST)的注入攻击分析实现
 * @author guyadong
 *
 */
public class InjectionAstNodeVisitor extends CCJSqlParserDefaultVisitor
    public InjectionAstNodeVisitor() 
    
    @Override
    public Object visit(SimpleNode node, Object data) 
        Object value = node.jjtGetValue();
        if(value instanceof UnionOp)
            throw new InjectionAttackException("DISABLE UNION");
        
        return super.visit(node, data);
    

单元测试

InjectAttackCheckerTest SQL注入攻击检查测试

import static org.junit.Assert.*;

import org.junit.BeforeClass;
import org.junit.Test;

import gu.sql2java.parser.InjectionAttackException;
import gu.sql2java.parser.ParserSupport;
import gu.sql2java.parser.SqlInjectionAnalyzer;
import gu.sql2java.parser.ParserSupport.SqlParserInfo;
import net.sf.jsqlparser.JSQLParserException;

import static gu.sql2java.SimpleLog.log;

/**
 * SQL注入攻击检查测试
 * @author guyadong
 *
 */
public class InjectAttackCheckerTest 

    private static SqlInjectionAnalyzer analyser;
    @BeforeClass
    public static void setUpBeforeClass() throws Exception 
        analyser= new SqlInjectionAnalyzer();
    
    private boolean injectAnalyse(String sql) throws JSQLParserException
        SqlParserInfo sqlParserInfo = ParserSupport.parse0(sql, null);
        try 
            analyser.injectAnalyse(sqlParserInfo);
            return true;
         catch (InjectionAttackException e) 
            log("",e.getMessage());
            //log(e);
            return false;
        
    
    @Test
    public void test() throws JSQLParserException 
        assertFalse(injectAnalyse("select * from dc_device where id in (select id from other)"));
        assertFalse(injectAnalyse("select * from dc_device where 2=2.0 or 2 != 4"));
        assertFalse(injectAnalyse("select * from dc_device where 1!=2.0"));
        assertFalse(injectAnalyse("select * from dc_device where id=floor(2.0)"));
        assertFalse(injectAnalyse("select * from dc_device where not true"));
        assertFalse(injectAnalyse("select * from dc_device where 1 or id > 0"));
        assertFalse(injectAnalyse("select * from dc_device where 'tom' or id > 0"));
        assertFalse(injectAnalyse("select * from dc_device where '-2.3' "));
        assertFalse(injectAnalyse("select * from dc_device where 2 "));
        assertFalse(injectAnalyse("select * from dc_device where (3+2) "));
        assertFalse(injectAnalyse("select * from dc_device where  -1 IS TRUE"));
        assertFalse(injectAnalyse("select * from dc_device where 'hello' is null "));
        assertFalse(injectAnalyse("select * from dc_device where '2022-10-31' and id > 0"));
        assertFalse(injectAnalyse("select * from dc_device where id > 0 or 1!=2.0 "));
        assertFalse(injectAnalyse("select * from dc_device where id > 0 or 1 in (1,3,4) "));
        assertFalse(injectAnalyse("select * from dc_device  UNION select name from other"));
        assertTrue(injectAnalyse("WITH SUB1 AS (SELECT user FROM t1) SELECT * FROM T2 WHERE id > 123 "));
    



完整代码参见码云仓库:
https://gitee.com/l0km/sql2java/tree/dev/sql2java-manager/src/main/java/gu/sql2java/parser

以上是关于jsqlparser:实现基于SQL语法分析的SQL注入攻击检查的主要内容,如果未能解决你的问题,请参考以下文章

jsqlparser:基于抽象语法树(AST)遍历SQL语句的语法元素

jsqlparser:实现MySQL 函数DATE_FORMAT到Phoenix函数TO_CHAR的替换

jsqlparser:修改语法定义(JSqlParserCC.jjt)实现UPSERT支持Phoenix语法ON DUPLICATE KEY IGNORE

jsqlparser:修改语法定义(JSqlParserCC.jjt)实现UPSERT支持Phoenix语法ON DUPLICATE KEY IGNORE

java sql解析器比较druid sql parser vs jsqlparser vs fdb-sql-parser

看这篇就够了丨基于Calcite框架的SQL语法扩展探索