Java - 转义字符串以防止 SQL 注入

Posted

技术标签:

【中文标题】Java - 转义字符串以防止 SQL 注入【英文标题】:Java - escape string to prevent SQL injection 【发布时间】:2010-12-21 05:42:32 【问题描述】:

我正在尝试在 java 中放置一些反 sql 注入,但发现使用“replaceAll”字符串函数非常困难。最终我需要一个函数来将任何现有的\ 转换为\\,将任何" 转换为\",将任何' 转换为\',以及将任何\n 转换为\\n,这样当字符串由 mysql SQL 注入评估将被阻止。

我已经提取了一些我正在使用的代码,并且函数中的所有\\\\\\\\\\\ 都让我的眼睛发疯了。如果有人碰巧有这样的例子,我将不胜感激。

【问题讨论】:

好的,我得出的结论是 PreparedStatements 是要走的路,但是根据当前的目标,我需要按照最初的计划进行,并且暂时放置一个过滤器一旦达到当前的里程碑,我就可以返回并重构数据库以获取preparedstatement。在保持势头的同时,考虑到 Java 和它的正则表达式系统,是否有人有一个解决方案可以有效地转义 MySQL 的上述字符,计算出所需的转义数量是绝对痛苦的...... 并非所有 SQL 语句都是可参数化的,例如“SET ROLE role_name”或“LISTEN channel_name” @NeilMcGuigan 是的。大多数驱动程序也会拒绝参数化 CREATE VIEW myview AS SELECT * FROM mytable WHERE col = ? 之类的东西,因为主要语句是 DDL-statement,即使您尝试参数化的部分实际上是 DML。 【参考方案1】:

防止 SQL 注入的唯一方法是使用参数化 SQL。根本不可能构建一个比以破解 SQL 为生的人更聪明的过滤器。

因此,对所有输入、更新和 where 子句都使用参数。动态 SQL 对黑客来说只是一扇敞开的大门,其中包括存储过程中的动态 SQL。参数化,参数化,参数化。

【讨论】:

甚至参数化的 SQL 也不是 100% 的保证。但这是一个很好的开始。 @duffymo,我同意没有什么是 100% 安全的。您是否有一个即使使用参数化 SQL 也可以工作的 SQL 注入示例? @Cylon Cat:当然,当一段 SQL(如 @WhereClause 或 @tableName)作为参数传递时,连接到 SQL 中并动态执行。当您让用户编写代码时,就会发生 SQL 注入。是否将他们的代码作为参数捕获并不重要。 顺便说一句,我不知道为什么没有更多地提及这一点,但是使用 PreparedStatements 也更加更容易并且更加更具可读性。仅这一点就可能使它们成为每个了解它们的程序员的默认设置。 请注意,某些数据库的 PreparedStatements 的创建成本非常高,因此如果您需要做很多,请同时测量这两种类型。【参考方案2】:

使用正则表达式删除可能导致 SQL 注入的文本听起来像是通过 Statement 而不是 PreparedStatement 将 SQL 语句发送到数据库。

首先防止 SQL 注入的最简单方法之一是使用 PreparedStatement,它接受数据以使用占位符替换到 SQL 语句中,它不依赖字符串连接来创建 SQL 语句以发送到数据库。

如需了解更多信息,The Java Tutorials 中的Using Prepared Statements 将是一个不错的起点。

【讨论】:

【参考方案3】:

PreparedStatements 是可行的方法,因为它们使 SQL 注入成为不可能。下面是一个以用户输入为参数的简单示例:

public insertUser(String name, String email) 
   Connection conn = null;
   PreparedStatement stmt = null;
   try 
      conn = setupTheDatabaseConnectionSomehow();
      stmt = conn.prepareStatement("INSERT INTO person (name, email) values (?, ?)");
      stmt.setString(1, name);
      stmt.setString(2, email);
      stmt.executeUpdate();
   
   finally 
      try 
         if (stmt != null)  stmt.close(); 
      
      catch (Exception e) 
         // log this error
      
      try 
         if (conn != null)  conn.close(); 
      
      catch (Exception e) 
         // log this error
      
   

无论姓名和电子邮件中的字符是什么,这些字符都将直接放入数据库中。它们不会以任何方式影响 INSERT 语句。

对于不同的数据类型有不同的设置方法——你使用哪一种取决于你的数据库字段是什么。例如,如果数据库中有一个 INTEGER 列,则应使用setInt 方法。 The PreparedStatement documentation 列出了所有可用于设置和获取数据的不同方法。

【讨论】:

通过这种方法,你能把每个参数都当作一个字符串,并且仍然安全吗?我正在尝试找出一种方法来更新我现有的架构以确保安全,而无需重建整个数据库层...... 所有动态 SQL 都只是字符串,所以这不是要问的问题。我不熟悉 PrepareStatement,所以真正的问题是它是否会生成一个参数化查询,然后可以使用 ExecuteUpdate 执行该查询。如果是,那很好。如果不是,那么它只是隐藏了问题,除了重新设计数据库层之外,您可能没有任何安全选项。处理 SQL 注入是您从一开始就必须设计的事情之一。这不是您以后可以轻松添加的内容。 如果您要插入 INTEGER 字段,则需要使用“setInt”。同样,其他数字数据库字段将使用其他设置器。我发布了一个指向列出所有 setter 类型的 PreparedStatement 文档的链接。 是 Cylon,PreparedStatements 生成参数化查询。 @Kaleb Brasee,谢谢。很高兴知道。这些工具在每个环境中都不同,但基本的答案是深入到参数化查询。【参考方案4】:

如果你真的不能使用Defense Option 1: Prepared Statements (Parameterized Queries) 或Defense Option 2: Stored Procedures,不要构建你自己的工具,使用OWASP Enterprise Security API。来自托管在 Google 代码上的OWASP ESAPI:

不要编写自己的安全控制!在为每个 Web 应用程序或 Web 服务开发安全控制时重新发明***会导致时间浪费和大量安全漏洞。 OWASP 企业安全 API (ESAPI) 工具包可帮助软件开发人员防范与安全相关的设计和实施缺陷。

有关详细信息,请参阅Preventing SQL Injection in Java 和SQL Injection Prevention Cheat Sheet。

特别注意Defense Option 3: Escaping All User Supplied Input,它介绍了OWASP ESAPI项目)。

【讨论】:

从今天开始,ESAPI 似乎已经失效。在 AWS 上有 WAF 可以帮助防止 SQL 注入、XSS 等。此时还有其他选择吗? @ChrisOdney 可以轻松绕过 WAF。大多数框架已经实现了自己的 SQL 注入预防,它们自己自动转义参数。遗留项目的替代方案:owasp.org/index.php/…【参考方案5】:

如果您正在处理遗留系统,或者您有太多地方可以在太短的时间内切换到PreparedStatements - 即如果使用其他答案建议的最佳实践存在障碍,您可以尝试AntiSQLFilter

【讨论】:

【参考方案6】:

(这是对 OP 在原始问题下的评论的回答;我完全同意 PreparedStatement 是这项工作的工具,而不是正则表达式。)

当您说\n 时,您是指序列\+n 还是实际的换行符?如果是\+n,任务就很简单了:

s = s.replaceAll("['\"\\\\]", "\\\\$0");

要匹配输入中的一个反斜杠,请将其中的四个放在正则表达式字符串中。要在输出中放置一个反斜杠,请将其中的四个放在替换字符串中。这是假设您正在以 Java 字符串文字的形式创建正则表达式和替换。如果您以任何其他方式创建它们(例如,通过从文件中读取它们),则不必进行所有双重转义。

如果您在输入中有换行符,并且您想用转义序列替换它,您可以使用以下命令对输入进行第二次传递:

s = s.replaceAll("\n", "\\\\n");

或者你可能想要两个反斜杠(我不太清楚):

s = s.replaceAll("\n", "\\\\\\\\n");

【讨论】:

感谢您的评论,我喜欢您将所有字符合二为一的方式,我打算用较少的正则表达式方式替换每个字符...我不知道如何现在分配这个问题的答案。最终 PreparedStatements 是答案,但对于我目前的目标,你的答案是我需要的答案,如果我对之前准备好的陈述的答案之一给出答案,你会不高兴,或者有没有办法在一对夫妇之间分享答案? 由于这只是一个临时的组合,请继续接受 PreparedStatement 答案之一。【参考方案7】:

PreparedStatements 是大多数情况下的方法,但不是所有情况。有时您会发现自己处于必须构建查询或查询的一部分并将其存储为字符串以供以后使用的情况。查看OWASP Site 上的SQL Injection Prevention Cheat Sheet,了解更多详细信息和不同编程语言的API。

【讨论】:

OWASP 备忘单已移至 GitHub。 SQL 注入备忘单现在在这里:github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/…【参考方案8】:

您需要以下代码。乍一看,这可能看起来像我编写的任何旧代码。但是,我所做的是查看http://grepcode.com/file/repo1.maven.org/maven2/mysql/mysql-connector-java/5.1.31/com/mysql/jdbc/PreparedStatement.java 的源代码。然后,我仔细查看了 setString(int parameterIndex, String x) 的代码以找到它转义的字符,并将其定制为我自己的类,以便它可以用于您需要的目的。毕竟,如果这是 Oracle 转义的字符列表,那么知道这一点在安全方面确实令人欣慰。也许 Oracle 需要推动为下一个主要 Java 版本添加类似于此的方法。

public class SQLInjectionEscaper 

    public static String escapeString(String x, boolean escapeDoubleQuotes) 
        StringBuilder sBuilder = new StringBuilder(x.length() * 11/10);

        int stringLength = x.length();

        for (int i = 0; i < stringLength; ++i) 
            char c = x.charAt(i);

            switch (c) 
            case 0: /* Must be escaped for 'mysql' */
                sBuilder.append('\\');
                sBuilder.append('0');

                break;

            case '\n': /* Must be escaped for logs */
                sBuilder.append('\\');
                sBuilder.append('n');

                break;

            case '\r':
                sBuilder.append('\\');
                sBuilder.append('r');

                break;

            case '\\':
                sBuilder.append('\\');
                sBuilder.append('\\');

                break;

            case '\'':
                sBuilder.append('\\');
                sBuilder.append('\'');

                break;

            case '"': /* Better safe than sorry */
                if (escapeDoubleQuotes) 
                    sBuilder.append('\\');
                

                sBuilder.append('"');

                break;

            case '\032': /* This gives problems on Win32 */
                sBuilder.append('\\');
                sBuilder.append('Z');

                break;

            case '\u00a5':
            case '\u20a9':
                // escape characters interpreted as backslash by mysql
                // fall through

            default:
                sBuilder.append(c);
            
        

        return sBuilder.toString();
    

【讨论】:

我认为这段代码是上面链接中源代码的反编译版本。现在在较新的 mysql-connector-java-xxx 中,case '\u00a5'case '\u20a9' 语句似乎已被删除 我用你的代码尝试了 sqlmap,但它没有保护我免受第一次攻击`类型:基于布尔的盲标题:基于布尔的盲 - WHERE 或 HAVING 子句有效负载:q=1%' AND 5430=5430 AND '%'='` 抱歉它的工作,但正在查看最后存储的会话结果..我保留了未来类似的评论.. 您可以使用org.ostermiller.utils.StringHelper.escapeSQL()com.aoindustries.sql.SQLUtility.escapeSQL() 重要的是要注意原始代码上的 GPLv2 许可证,这是为遇到此问题的任何人复制的。我不是律师,但我强烈建议您不要在您的项目中使用此答案,除非您完全了解包含此许可代码的含义。【参考方案9】:

Prepared Statements 是最好的解决方案,但如果您真的需要手动执行,您也可以使用 Apache Commons-Lang 库中的 StringEscapeUtils 类。它有一个escapeSql(String) 方法,你可以使用它:

import org.apache.commons.lang.StringEscapeUtils; … String escapedSQL = StringEscapeUtils.escapeSql(unescapedSQL);

【讨论】:

供参考:commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/… 反正这个方法只是转义引号,似乎并不能防止SQL注入攻击。 这已从最新版本中删除,因为它只是转义单引号 这个答案应该被删除,因为它不能防止sql注入。【参考方案10】:

在搜索了很多防止sqlmap注入sql的解决方案后,以防遗留系统无法在每个地方应用准备好的语句。

java-security-cross-site-scripting-xss-and-sql-injection topic 解决方案

我尝试了@Richard 的解决方案,但在我的情况下不起作用。 我用了一个过滤器

此过滤器的目标是将请求包装成自己编码的 包装器 MyHttpRequestWrapper 转换:

将带有特殊字符(、'、...)的 HTTP 参数转换为 html 通过 org.springframework.web.util.HtmlUtils.htmlEscape(...) 编写代码 方法。注意:Apache Commons 中有类似的类: org.apache.commons.lang.StringEscapeUtils.escapeHtml(…) SQL 通过 Apache Commons 类的注入字符('、“、...) org.apache.commons.lang.StringEscapeUtils.escapeSql(…)

<filter>
<filter-name>RequestWrappingFilter</filter-name>
<filter-class>com.huo.filter.RequestWrappingFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>RequestWrappingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>




package com.huo.filter;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletReponse;
import javax.servlet.http.HttpServletRequest;

public class RequestWrappingFilter implements Filter

    public void doFilter(ServletRequest req, ServletReponse res, FilterChain chain) throws IOException, ServletException
        chain.doFilter(new MyHttpRequestWrapper(req), res);
    

    public void init(FilterConfig config) throws ServletException
    

    public void destroy() throws ServletException
    





package com.huo.filter;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.lang.StringEscapeUtils;

public class MyHttpRequestWrapper extends HttpServletRequestWrapper
    private Map<String, String[]> escapedParametersValuesMap = new HashMap<String, String[]>();

    public MyHttpRequestWrapper(HttpServletRequest req)
        super(req);
    

    @Override
    public String getParameter(String name)
        String[] escapedParameterValues = escapedParametersValuesMap.get(name);
        String escapedParameterValue = null; 
        if(escapedParameterValues!=null)
            escapedParameterValue = escapedParameterValues[0];
        else
            String parameterValue = super.getParameter(name);

            // HTML transformation characters
            escapedParameterValue = org.springframework.web.util.HtmlUtils.htmlEscape(parameterValue);

            // SQL injection characters
            escapedParameterValue = StringEscapeUtils.escapeSql(escapedParameterValue);

            escapedParametersValuesMap.put(name, new String[]escapedParameterValue);
        //end-else

        return escapedParameterValue;
    

    @Override
    public String[] getParameterValues(String name)
        String[] escapedParameterValues = escapedParametersValuesMap.get(name);
        if(escapedParameterValues==null)
            String[] parametersValues = super.getParameterValues(name);
            escapedParameterValue = new String[parametersValues.length];

            // 
            for(int i=0; i<parametersValues.length; i++)
                String parameterValue = parametersValues[i];
                String escapedParameterValue = parameterValue;

                // HTML transformation characters
                escapedParameterValue = org.springframework.web.util.HtmlUtils.htmlEscape(parameterValue);

                // SQL injection characters
                escapedParameterValue = StringEscapeUtils.escapeSql(escapedParameterValue);

                escapedParameterValues[i] = escapedParameterValue;
            //end-for

            escapedParametersValuesMap.put(name, escapedParameterValues);
        //end-else

        return escapedParameterValues;
    

【讨论】:

java-security-cross-site-scripting-xss-and-sql-injection topic 好吗?我正在尝试为遗留应用程序寻找解决方案。【参考方案11】:

发件人:Source

public String MysqlRealScapeString(String str)
  String data = null;
  if (str != null && str.length() > 0) 
    str = str.replace("\\", "\\\\");
    str = str.replace("'", "\\'");
    str = str.replace("\0", "\\0");
    str = str.replace("\n", "\\n");
    str = str.replace("\r", "\\r");
    str = str.replace("\"", "\\\"");
    str = str.replace("\\x1a", "\\Z");
    data = str;
  
  return data;

【讨论】:

警告:它会为空字符串返回 null,因此您可能会得到意想不到的结果:"SELECT ITEM.id FROM ITEM WHERE ITEM.value = '" + MysqlRealScapeString(text) + "'" 将向您显示 ITEM.value = 'null' 而不是 ITEM.value = '' 的项目! 【参考方案12】:

如果你使用 PL/SQL,你也可以使用DBMS_ASSERT 它可以清理您的输入,因此您可以使用它而不必担心 SQL 注入。

例如看这个答案: https://***.com/a/21406499/1726419

【讨论】:

【参考方案13】:

首先,问一个问题 - 用户输入字段中需要双引号还是单引号或反斜杠?

反斜杠 - 不。双引号和单引号在英语中很少使用,它们在英国的使用方式与美国不同。

我说删除或替换它们,然后你就简化了。

private String scrub(
    String parameter,
    int    length
    )

    String parm = null;

    if ( parameter != null && parameter.length() > 0 && parameter.length() < length )
    
        parm = parameter
            .replace( "\\", " " )
            .replace( "\"", " " )
            .replace( "\'", " " )
            .replace( "\t", " " )
            .replace( "\r", " " )
            .replace( "\n", " " )
            .trim();
    

    return parm;

【讨论】:

建议不保留输入的转换是一个坏主意。 试想一下,如果在 Stack Overflow 上使用这种方法,您的答案会是什么样子。【参考方案14】:

大多数人都推荐PreparedStatements,但这需要您使用Java 应用程序与您的数据库直接连接。但是你会听到其他人说,由于安全问题,你不应该直接连接到你的数据库,而是使用 Restful API 来处理查询。

在我看来,只要你意识到你必须小心你逃避和故意做的事情,应该没有问题。

我的解决方案是使用contains() 来检查SQL 关键字,例如UPDATE 或其他危险字符,例如=,通过要求用户在输入中插入其他字符来完全消除SQL 注入。

编辑: 您可以使用来自 W3Schools 的关于 Java Regular Expressions 的源材料对字符串进行验证。

【讨论】:

以上是关于Java - 转义字符串以防止 SQL 注入的主要内容,如果未能解决你的问题,请参考以下文章

如何防止 SQL 注入转义字符串

JAVA中转义字元的疑问

防止XSS注入的方法

vue内容都会被自动转义,所以防止了脚本注入吗?

addslashes,htmlspecialchars,htmlentities转换或者转义php特殊字符防止xss攻击以及sql注入

SQL语句中转义字符怎么写?