【问题标题】:How can prepared statements protect from SQL injection attacks?准备好的语句如何防止 SQL 注入攻击?
【发布时间】:2012-01-05 23:53:20
【问题描述】:

prepared statements 如何帮助我们防止SQL injection 攻击?

维基百科说:

准备好的语句可以抵御 SQL 注入,因为 参数值,稍后使用不同的 协议,不需要正确转义。如果原来的说法 模板不是从外部输入派生的,SQL注入不能 发生。

我不能很好地理解原因。用简单的英语和一些例子来简单的解释是什么?

【问题讨论】:

    标签: sql security sql-injection prepared-statement


    【解决方案1】:

    关键字是need not be correctly escaped。这意味着您不必担心人们会尝试插入破折号、撇号、引号等...

    一切都为你处理。

    【讨论】:

      【解决方案2】:
      ResultSet rs = statement.executeQuery("select * from foo where value = " + httpRequest.getParameter("filter");
      

      假设你在一个 Servlet 中拥有它是对的。如果恶意人员传递了错误的“过滤器”值,您可能会入侵您的数据库。

      【讨论】:

        【解决方案3】:

        这是一个用于设置示例的 SQL 语句:

        CREATE TABLE employee(name varchar, paymentType varchar, amount bigint);
        
        INSERT INTO employee VALUES('Aaron', 'salary', 100);
        INSERT INTO employee VALUES('Aaron', 'bonus', 50);
        INSERT INTO employee VALUES('Bob', 'salary', 50);
        INSERT INTO employee VALUES('Bob', 'bonus', 0);
        

        Inject 类易受 SQL 注入攻击。查询与用户输入一起动态粘贴。查询的目的是显示有关 Bob 的信息。根据用户输入,工资或奖金。但是恶意用户通过在 where 子句中添加相当于“或真”的内容来操纵输入,从而破坏查询,从而返回所有内容,包括本应隐藏的关于 Aaron 的信息。

        import java.sql.*;
        
        public class Inject {
        
            public static void main(String[] args) throws SQLException {
        
                String url = "jdbc:postgresql://localhost/postgres?user=user&password=pwd";
                Connection conn = DriverManager.getConnection(url);
        
                Statement stmt = conn.createStatement();
                String sql = "SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='" + args[0] + "'";
                System.out.println(sql);
                ResultSet rs = stmt.executeQuery(sql);
        
                while (rs.next()) {
                    System.out.println(rs.getString("paymentType") + " " + rs.getLong("amount"));
                }
            }
        }
        

        运行这个,第一种是正常使用,第二种是恶意注入:

        c:\temp>java Inject salary
        SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='salary'
        salary 50
        
        c:\temp>java Inject "salary' OR 'a'!='b"
        SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType='salary' OR 'a'!='b'
        salary 100
        bonus 50
        salary 50
        bonus 0
        

        您不应使用用户输入的字符串连接来构建 SQL 语句。它不仅容易受到注入攻击,而且对服务器也有缓存影响(语句更改,因此不太可能获得 SQL 语句缓存命中,而绑定示例始终运行相同的语句)。

        这是一个避免这种注入的Binding示例:

        import java.sql.*;
        
        public class Bind {
        
            public static void main(String[] args) throws SQLException {
        
                String url = "jdbc:postgresql://localhost/postgres?user=postgres&password=postgres";
                Connection conn = DriverManager.getConnection(url);
        
                String sql = "SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?";
                System.out.println(sql);
        
                PreparedStatement stmt = conn.prepareStatement(sql);
                stmt.setString(1, args[0]);
        
                ResultSet rs = stmt.executeQuery();
        
                while (rs.next()) {
                    System.out.println(rs.getString("paymentType") + " " + rs.getLong("amount"));
                }
            }
        }
        

        使用与上一个示例相同的输入运行此代码表明恶意代码不起作用,因为没有与该字符串匹配的 paymentType:

        c:\temp>java Bind salary
        SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?
        salary 50
        
        c:\temp>java Bind "salary' OR 'a'!='b"
        SELECT paymentType, amount FROM employee WHERE name = 'bob' AND paymentType=?
        

        【讨论】:

        • 在连接到数据库的程序中使用准备好的语句是否与使用作为数据库一部分的准备好的语句具有相同的效果?例如 Postgres 有它自己的准备好的语句,并且会使用它来防止 SQL 注入? postgresql.org/docs/9.2/static/sql-prepare.html
        • @Celeritas 我对 Postgresql 没有明确的答案。查看文档,看起来效果是一样的。 PREPARE 创建一个已解析的固定命名语句(即,无论输入如何,该语句都不会再更改),而 EXECUTE 将运行绑定参数的命名语句。由于PREPARE 仅具有会话持续时间,因此看起来它确实是出于性能原因,而不是用于防止通过 psql 脚本进行注入。对于 psql 访问,可以授予存储过程的权限并在 procs 中绑定参数。
        • @Celeritas 我在 x86_64 上使用 PostgreSQL 11.1 尝试了上面的代码,上面的 SQLi 示例有效。
        【解决方案4】:

        这个想法很简单 - 查询和数据分别发送到数据库服务器
        就是这样。

        SQL注入问题的根源在于代码和数据的混合。

        事实上,我们的 SQL 查询是合法程序。 我们正在动态创建这样一个程序,动态添加一些数据。因此,数据可能会干扰程序代码,甚至改变它,正如每个 SQL 注入示例所显示的那样(PHP/Mysql 中的所有示例):

        $expected_data = 1;
        $query = "SELECT * FROM users where id=$expected_data";
        

        将产生一个常规查询

        SELECT * FROM users where id=1
        

        这段代码

        $spoiled_data = "1; DROP TABLE users;"
        $query        = "SELECT * FROM users where id=$spoiled_data";
        

        会产生恶意序列

        SELECT * FROM users where id=1; DROP TABLE users;
        

        之所以有效,是因为我们将数据直接添加到程序主体中,并且它成为程序的一部分,因此数据可能会改变程序,并且根据传递的数据,我们将获得常规输出或表格users已删除。

        虽然在准备好的语句的情况下,我们不会改变我们的程序,但它保持不变
        这才是重点。

        我们首先向服务器发送一个程序

        $db->prepare("SELECT * FROM users where id=?");
        

        数据被一些称为参数或占位符的变量替换。

        请注意,将完全相同的查询发送到服务器,其中没有任何数据!然后我们通过 second 请求发送数据,基本上与查询本身分开:

        $db->execute($data);
        

        所以它不会改变我们的程序并造成任何伤害。
        很简单——不是吗?

        我要补充的唯一一点在每本手册中总是省略:

        准备好的语句只能保护数据文字,但不能与任何其他查询部分一起使用。
        因此,一旦我们必须添加一个动态的标识符——例如一个字段名——准备好的语句就帮不了我们了。我有explained the matter recently,所以我不会重复自己。

        【讨论】:

        • “例如,默认情况下 PDO 不使用准备好的语句” - 这并不完全正确,因为 PDO 仅对不支持此类功能的驱动程序模拟准备好的语句。
        • @zaq178miami:“PDO 仅模拟不支持该功能的驱动程序的准备好的语句”- 不完全正确。 MySQL 支持prepared statements 已经有一段时间了。 PDO 驱动程序也有。但是,我上次检查时,默认情况下,MySQL 查询仍然由 PDO 准备。
        • $spoiled_data = "1; DROP TABLE users;" -> $query = "SELECT * FROM users where id=$spoiled_data";$db->prepare("SELECT * FROM users where id=?"); 相比有什么不同 -> $data = "1; DROP TABLE users;" -> $db->execute($data);。他们不会做同样的事情吗?
        • @Juha Untinen 数据可以是任何东西。它不会解析数据。那是 DATA 而不是命令。所以即使 $data 中包含 sql 命令,也不会被执行。此外,如果 id 是数字,则字符串内容将生成报告或值为零。
        • 你将如何将准备好的语句用于动态列名或 %% 语句?示例:'select * from table where ?像 %?%' 以我的经验,这不起作用。
        【解决方案5】:

        SQL Server 中,使用准备好的语句绝对是防注入的,因为输入参数不会形成查询。这意味着执行的查询不是动态查询。 SQL 注入漏洞语句示例。

        string sqlquery = "select * from table where username='" + inputusername +"' and password='" + pass + "'";
        

        现在,如果 inoutusername 变量中的值类似于 a' 或 1=1 --,则此查询现在变为:

        select * from table where username='a' or 1=1 -- and password=asda
        

        其余部分在-- 之后注释,因此它永远不会被执行和绕过,因为使用下面的准备好的语句示例。

        Sqlcommand command = new sqlcommand("select * from table where username = @userinput and password=@pass");
        command.Parameters.Add(new SqlParameter("@userinput", 100));
        command.Parameters.Add(new SqlParameter("@pass", 100));
        command.prepare();
        

        所以实际上你不能发送另一个参数,从而避免 SQL 注入...

        【讨论】:

          【解决方案6】:

          基本上,对于准备好的语句,来自潜在黑客的数据被视为数据 - 并且它不可能与您的应用程序 SQL 混合和/或被解释为 SQL(当传入的数据被放置时可能会发生这种情况直接进入您的应用程序 SQL)。

          这是因为准备好的语句首先“准备”SQL 查询以找到有效的查询计划,然后发送可能来自表单的实际值 - 那时查询实际执行。

          更多精彩信息在这里:

          Prepared statements and SQL Injection

          【讨论】:

            【解决方案7】:

            当您创建准备好的语句并将其发送到 DBMS 时,它会存储为 SQL 查询以供执行。

            您稍后将数据绑定到查询,以便 DBMS 使用该数据作为执行(参数化)的查询参数。 DBMS 不会使用您绑定的数据作为已编译 SQL 查询的补充;这只是数据。

            这意味着使用预准备语句执行 SQL 注入基本上是不可能的。准备好的语句的本质及其与 DBMS 的关系阻止了这种情况。

            【讨论】:

              【解决方案8】:

              根本原因 #1 - 分隔符问题

              Sql 注入是可能的,因为我们使用引号来分隔字符串并且也是字符串的一部分,因此有时无法解释它们。如果我们有不能在字符串数据中使用的分隔符,sql 注入永远不会发生。解决定界符问题就消除了sql注入问题。结构查询就是这样做的。

              根本原因 #2 - 人性,人是狡猾的,有些狡猾的人是恶意的 所有人都会犯错误

              sql注入的另一个根本原因是人性。包括程序员在内的人都会犯错误。当您在结构化查询中出错时,它不会使您的系统容易受到 sql 注入的影响。如果不使用结构化查询,错误可能会产生 sql 注入漏洞。

              结构化查询如何解决 SQL 注入的根本原因

              结构化查询通过将 sql 命令放在一个语句中并将数据放在单独的编程语句中来解决分隔符问题。编程语句创建所需的分隔。

              结构化查询有助于防止人为错误造成严重的安全漏洞。 关于人为错误,使用结构查询时不会发生sql注入。有一些方法可以防止不涉及结构化查询的 sql 注入,但是这种方法中的正常人为错误通常会导致至少一些暴露于 sql 注入。结构化查询不会受到 sql 注入的影响。你几乎可以用结构化查询犯世界上所有的错误,就像任何其他编程一样,但你犯的任何错误都不能变成被 sql 注入接管的 ssstem。这就是为什么人们喜欢说这是防止sql注入的正确方法。

              那么,你知道了 sql 注入的原因以及在使用时使它们无法使用的结构化查询的性质。

              【讨论】:

                【解决方案9】:

                我通读了答案,仍然觉得有必要强调阐明准备好的陈述本质的关键点。考虑两种查询涉及用户输入的数据库的方法:

                天真的方法

                将用户输入与部分 SQL 字符串连接起来生成 SQL 语句。在这种情况下,用户可以嵌入恶意 SQL 命令,然后将其发送到数据库执行。

                String SQLString = "SELECT * FROM CUSTOMERS WHERE NAME='"+userInput+"'"
                

                例如,恶意用户输入会导致SQLString等于"SELECT * FROM CUSTOMERS WHERE NAME='James';DROP TABLE CUSTOMERS;'

                由于恶意用户,SQLString 包含 2 条语句,其中第 2 条 ("DROP TABLE CUSTOMERS") 会造成伤害。

                准备好的陈述

                在这种情况下,由于查询和数据的分离,用户输入永远不会被视为 SQL 语句,因此永远不会执行。正是由于这个原因,任何注入的恶意 SQL 代码都不会造成任何伤害。所以"DROP TABLE CUSTOMERS" 在上述情况下永远不会被执行。

                简而言之,通过用户输入引入的带有预处理语句的恶意代码将不会被执行!

                【讨论】:

                • 真的吗?接受的答案并不能完全说明这一点?
                • @Your Common Sense 接受的答案充满了很多有价值的信息,但它让我想知道数据和查询分离的实现细节需要什么。而关注恶意注入的数据(如果有的话)永远不会被执行这一点却是一针见血。
                • 您的答案中提供了哪些“实施细节”没有出现?
                • 如果您尝试了解我的出发点,您会意识到我的观点如下:看到实现细节的短暂愿望源于需要了解为什么恶意用户输入不会造成任何伤害。不太需要查看实现细节。这就是为什么意识到实现细节是这样的,在任何时候都不会执行恶意输入的 SQL,将消息发送回家。您的回答回答了这个问题,如何(按要求)?但我想其他人(比如我)会对为什么的简洁回答感到满意?
                • 说数据是与查询分开发送的,并且程序保持不变,但仍然没有说明如何准确地防止损害。是不是因为查询部分在执行前被筛选了?是因为查询从未执行吗?这正是引发了想了解幕后究竟发生了什么的愿望的思考过程。我相信我的回答回答了这个问题并得到了赞成票,然后是反对票(我猜来自你),希望你能明白为什么这对其他人有用。
                猜你喜欢
                • 2012-06-19
                • 2015-09-06
                • 2020-09-28
                • 2018-07-05
                • 2012-08-07
                相关资源
                最近更新 更多