使用检查样式防止准备好的语句泄漏

Posted

技术标签:

【中文标题】使用检查样式防止准备好的语句泄漏【英文标题】:Preventing Prepared Statement Leaks using Checkstyles 【发布时间】:2011-07-21 16:38:16 【问题描述】:

假设我有以下代码:

PreparedStatement ps = null;
ResultSet rs = null;
try 
  ps = conn.createStatement(myQueryString);
  rs = ps.executeQuery();
  // process the results...
 catch (java.sql.SQLException e) 
  log.error("an error!", e);
  throw new Exception("I'm sorry. Your query did not work.");
 finally 
  ps.close();   // if we forgot to do this we leak
  rs.close();   // if we forgot to do this we leak

我希望捕捉到我忘记使用 Checkstyles 关闭 PreparedStatementResultSet 的情况。这可能吗?如果可以,我该怎么做?

【问题讨论】:

【参考方案1】:

PMD 和 Findbugs 都有针对 PreparedStatements(以及 ResultSets 和 Connections)的警告。我建议将它们用于此类警告,因为 CheckStyle 与代码样式有关,而不是查找诸如此类的数据流错误。

【讨论】:

但是 checkstyles 会为您的程序构建 AST,所以您不能使用自定义检查来做到这一点吗? 另外值得注意的是,我是在自动化中运行它,而不是在我的 IDE 中。 当然,如果您想花时间编写自定义检查,而不是仅仅重新使用已经存在的东西。我更喜欢使用 Findbugs/PMD/Checkstyle 相互重叠,因为每个都有一些其他人没有的检查 - 而不是只选择一个。 Findbugs 和 PMD 可以像 Checkstyle 一样运行和生成报告。 能否提供 PMD 和 FindBugs 中的规则参考? 我会推荐使用 Findbugs。要检测这种类型的数据流错误,需要有类型信息。 Checkstyle 检查无法访问此信息,而 Findbugs 检测器则可以。任何使用 Checkstyle 进行此检查的尝试都是“hack”。【参考方案2】:

我们创建了一个自定义 Checkstyle Check 来防止这些语句泄漏。代码在下面。 checkstyle 的美妙之处在于您可以使用公开 Java AST 的API 自定义您的检查。 我们创建了数十种自定义检查。一旦掌握了要点,创建新支票就很容易了。 我们还创建了一个 Subversion 预提交挂钩,它运行检查并防止违规代码进入存储库。开发人员会收到一条明确的消息(请参阅下面的“日志”调用),指示问题和线路。

import java.util.List;

import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;

/**
 * Code blocks that invoke Connection.prepareStatement(...), Connection.prepareCall(...) or
 * Connection.createStatement(...) must have a finally block
 * in which there is a call to ContextObject.closeStatement(Statement).
 */
public class CheckCloseStatement extends CheckTcu 

    @Override
    public int[] getDefaultTokens() 
        return new int[] TokenTypes.ASSIGN;
    

    @Override
    public void visitToken(DetailAST aAST) 
        DetailAST literalTry;
        DetailAST literalFinally;
        DetailAST paramCloseStmt;
        List<DetailAST> idents;
        List<DetailAST> identsInFinally;
        String stmtVarName;

        idents = findAllAstsOfType(aAST, TokenTypes.IDENT);
        for (DetailAST ident : idents) 

            if ((ident.getText().equals("prepareStatement") || ident.getText().equals("createStatement") ||
                    ident.getText().equals("prepareCall")) && ident.getParent().getType() == TokenTypes.DOT) 
                // a Statement is created in this assignment statement

                boolean violationFound = true;

                // look for the surrounding try statement
                literalTry = ident;
                do 
                    literalTry = literalTry.getParent();
                 while (literalTry != null && literalTry.getType() != TokenTypes.LITERAL_TRY);

                if (literalTry != null) 
                    // good, the Statement creating assignment is within a try block
                    // now look for the corresponding finally block
                    literalFinally = literalTry.findFirstToken(TokenTypes.LITERAL_FINALLY);

                    if (literalFinally != null) 
                        // good, there is a finally block
                        identsInFinally = findAllAstsOfType(literalFinally, TokenTypes.IDENT);
                        for (DetailAST identInFinally : identsInFinally) 
                            if (identInFinally.getText().equals("closeStatement")) 
                                // good, there's a call to my closeStatement method
                                paramCloseStmt =
                                        findFirstAstOfType(identInFinally.getParent().getNextSibling(), TokenTypes.IDENT);
                                stmtVarName = findFirstAstOfType(aAST, TokenTypes.IDENT).getText();
                                if (stmtVarName.equals(paramCloseStmt.getText())) 
                                    // great, closeStatement closes the Statement variable originally assigned
                                    violationFound = false;
                                    break;
                                
                            
                        
                    
                
                // Exception: this rule does not apply to Xyz and its subclasses (which have
                // the same name Xyz followed by a suffix)
                if (violationFound) 
                    DetailAST classDef = aAST;
                    do 
                        classDef = classDef.getParent();
                     while (classDef != null && classDef.getType() != TokenTypes.CLASS_DEF);
                    if (classDef != null) 
                        String className = classDef.findFirstToken(TokenTypes.IDENT).getText();
                        if (className.startsWith("Xyz")) 
                            violationFound = false;
                        
                    
                
                if (violationFound) 
                    log(ident.getLineNo(),
                            "Code blocks that call Connection.prepareStatement(...) or Connection.prepareCall(...) " +
                                    "need a finally block where you should call ContextObject.closeStatement(Statement).");
                
            
        
    

此自定义检查扩展了一个抽象类,其中包含如下所示的两个实用方法。

import java.util.ArrayList;
import java.util.List;

import com.puppycrawl.tools.checkstyle.api.Check;
import com.puppycrawl.tools.checkstyle.api.DetailAST;

/**
 * Utility methods used in custom checks. 
 */
public abstract class CheckTcu extends Check 

    /**
     * Recursively traverse an expression tree and return all ASTs matching a specific token type.
     * 
     * @return list of DetailAST objects found; returns empty List if none is found.
     */
    protected List<DetailAST> findAllAstsOfType(DetailAST parent, int type) 
        List<DetailAST> children = new ArrayList<DetailAST>();

        DetailAST child = parent.getFirstChild();
        while (child != null) 
            if (child.getType() == type) 
                children.add(child);
             else 
                children.addAll(findAllAstsOfType(child, type));
            
            child = child.getNextSibling();
        
        return children;
    

    /**
     * Recursively traverse an expression tree and return the first AST matching a specific token type.
     * 
     * @return first DetailAST found or null if no AST of the given type is found
     */
    protected DetailAST findFirstAstOfType(DetailAST parent, int type) 
        DetailAST firstAst = null;

        DetailAST child = parent.getFirstChild();
        while (child != null) 
            if (child.getType() == type) 
                firstAst = child;
                break;
            
            DetailAST grandChild = findFirstAstOfType(child, type);
            if (grandChild != null) 
                firstAst = grandChild;
                break;
            
            child = child.getNextSibling();
        
        return firstAst;
    


【讨论】:

以上是关于使用检查样式防止准备好的语句泄漏的主要内容,如果未能解决你的问题,请参考以下文章

在 MySQL 中使用准备好的语句可以防止 SQL 注入攻击吗?

在关闭准备好的语句之前在函数中返回值 - 内存泄漏?

Hsqldb - 可能的准备语句内存泄漏

是否可以在没有准备好的语句(Node.js 和 MSSQL)的情况下防止 SQL 注入

PDO 准备好的语句如何帮助防止 SQL 易受攻击的语句?

如何防止准备好的语句更新 TIMESTAMP 列?