我如何防止PHP中的SQL注入?

如果用户输入未经修改而插入到SQL查询中,则该应用程序容易受到SQL注入的影响,如下例所示:

$unsafe_variable = $_POST['user_input']; 

mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");

这是因为用户可以输入类似value'); DROP TABLE table;--东西value'); DROP TABLE table;-- value'); DROP TABLE table;-- ,查询变为:

INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')

可以做些什么来防止这种情况发生?


使用预准备语句和参数化查询。 这些是由数据库服务器独立于任何参数发送并解析的SQL语句。 这样攻击者不可能注入恶意SQL。

你基本上有两个选择来实现这一点:

  • 使用PDO(用于任何支持的数据库驱动程序):

    $stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
    
    $stmt->execute(array('name' => $name));
    
    foreach ($stmt as $row) {
        // do something with $row
    }
    
  • 使用MySQLi(用于MySQL):

    $stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
    $stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
    
    $stmt->execute();
    
    $result = $stmt->get_result();
    while ($row = $result->fetch_assoc()) {
        // do something with $row
    }
    
  • 如果你连接的是MySQL以外的数据库,那么可以引用一个特定于驱动程序的第二个选项(例如PostgreSQL的pg_prepare()pg_execute() )。 PDO是普遍的选择。

    正确设置连接

    请注意,使用PDO访问MySQL数据库时,默认情况下不会使用实际准备的语句。 要解决这个问题,你必须禁用已准备好的语句的模拟。 使用PDO创建连接的示例是:

    $dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');
    
    $dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    $dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    

    在上面的例子中,错误模式并不是绝对必要的, 但建议添加它 。 这样,当出现Fatal Error时,脚本不会因Fatal Error而停止。 它使开发人员有机会catch throw n作为PDOException s的任何错误。

    但是, 必须强制的是第一个setAttribute()行,它告诉PDO禁用模拟的准备语句并使用实际准备的语句。 这可以确保PHP在将语句和值发送到MySQL服务器之前不会对其进行语法分析(使攻击者无法注入恶意SQL)。

    尽管可以在构造函数的选项中设置charset ,但需要注意的是,'旧'版本的PHP(<5.3.6)默默地忽略了DSN中的charset参数。

    说明

    会发生什么情况是,您传递给prepare的SQL语句由数据库服务器进行分析和编译。 通过指定参数(在上面的例子中,指定一个?或一个名为参数的:name ),就可以告诉数据库引擎您要过滤的位置。 然后,当您调用execute ,准备好的语句将与您指定的参数值组合在一起。

    这里重要的是参数值与编译语句组合,而不是SQL字符串。 SQL注入的工作原理是在创建SQL发送到数据库时,欺骗脚本使其包含恶意字符串。 因此,通过将实际的SQL与参数分开发送,您可以限制结束于您不想要的事情的风险。 使用预处理语句时发送的任何参数都将被视为字符串(尽管数据库引擎可能会进行一些优化,因此参数最终也可能会以数字结尾)。 在上面的例子中,如果$name变量包含'Sarah'; DELETE FROM employees 'Sarah'; DELETE FROM employees结果将仅仅是搜索字符串"'Sarah'; DELETE FROM employees" ,并且不会以空表结束。

    使用预准备语句的另一个好处是,如果您在同一个会话中多次执行相同的语句,那么它只会被解析和编译一次,从而使您获得一些速度提升。

    呵呵,既然你问过如何做插入,下面是一个例子(使用PDO):

    $preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');
    
    $preparedStatement->execute(array('column' => $unsafeValue));
    

    可以准备好的语句用于动态查询吗?

    虽然您仍然可以为查询参数使用准备好的语句,但动态查询本身的结构不能参数化,并且某些查询功能无法进行参数化。

    对于这些特定场景,最好的做法是使用限制可能值的白名单过滤器。

    // Value whitelist
    // $dir can only be 'DESC' otherwise it will be 'ASC'
    if (empty($dir) || $dir !== 'DESC') {
       $dir = 'ASC';
    }
    

    警告:此答案的示例代码(如问题的示例代码)使用PHP的mysql扩展,该扩展在PHP 5.5.0中被弃用,并且完全在PHP 7.0.0中被删除。

    如果您使用的是PHP的最新版本,则下面列出的mysql_real_escape_string选项将不再可用(尽管mysqli::escape_string是现代等效的)。 现在, mysql_real_escape_string选项只适用于老版本PHP上的旧代码。


    您有两个选项 - 转义unsafe_variable的特殊字符,或使用参数化查询。 两者都可以保护您免受SQL注入。 参数化查询被认为是更好的做法,但在使用它之前,需要在PHP中更改为较新的mysql扩展。

    我们将首先覆盖较低的撞击弦。

    //Connect
    
    $unsafe_variable = $_POST["user-input"];
    $safe_variable = mysql_real_escape_string($unsafe_variable);
    
    mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
    
    //Disconnect
    

    另请参阅mysql_real_escape_string函数的详细信息。

    要使用参数化查询,您需要使用MySQLi而不是MySQL函数。 重写您的示例,我们需要类似以下内容。

    <?php
        $mysqli = new mysqli("server", "username", "password", "database_name");
    
        // TODO - Check that connection was successful.
    
        $unsafe_variable = $_POST["user-input"];
    
        $stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");
    
        // TODO check that $stmt creation succeeded
    
        // "s" means the database expects a string
        $stmt->bind_param("s", $unsafe_variable);
    
        $stmt->execute();
    
        $stmt->close();
    
        $mysqli->close();
    ?>
    

    你需要阅读的关键函数是mysqli::prepare

    另外,正如其他人所建议的那样,您可能会发现使用PDO之类的方法来提升抽象层次会更有用/更容易。

    请注意,您询问的情况非常简单,而且更复杂的情况可能需要更复杂的方法。 尤其是:

  • 如果你想根据用户输入来改变SQL的结构,那么参数化查询不会有帮助,并且所需的转义不被mysql_real_escape_string覆盖。 在这种情况下,您最好将用户的输入传递给白名单,以确保只允许“安全”值。
  • 如果在条件中使用来自用户输入的整数并采用mysql_real_escape_string方法,则在下面的注释中将会遇到Polynomial描述的问题。 这种情况很棘手,因为整数不会被引号包围,所以你可以通过验证用户输入只包含数字来处理。
  • 有可能我还没有意识到的其他情况。 您可能会发现这是您可以遇到的一些更微妙问题的有用资源。

  • 这里的每个答案都只涵盖了部分问题。
    实际上,我们可以动态添加四个不同的查询部分:

  • 一个字符串
  • 一个号码
  • 一个标识符
  • 一个语法关键字。
  • 准备的报表只涵盖其中的2个

    但有时我们必须使我们的查询更具动态性,并添加运算符或标识符。
    所以,我们需要不同的保护技术。

    通常,这种保护方法基于白名单。 在这种情况下,每个动态参数都应该在脚本中进行硬编码并从该组中进行选择。
    例如,做动态排序:

    $orders  = array("name","price","qty"); //field names
    $key     = array_search($_GET['sort'],$orders)); // see if we have such a name
    $orderby = $orders[$key]; //if not, first one will be set automatically. smart enuf :)
    $query   = "SELECT * FROM `table` ORDER BY $orderby"; //value is safe
    

    但是,还有另一种方法来保护标识符 - 转义。 只要你有一个引用标识符,你可以通过将它们加倍来避开引号。

    作为更进一步的一步,我们可以借用一个真正高明的想法,从准备好的语句中使用一些占位符(一个代理来表示查询中的实际值),并创建另一种类型的占位符 - 一个标识符占位符。

    因此,长话短说:这是一个占位符 ,没有准备好的声明可以被视为银弹。

    所以,一般的建议可能被视为
    只要您使用占位符向查询中添加动态部分(并且正确处理这些占位符当然),就可以确保您的查询是安全的

    尽管如此,SQL语法关键字存在一个问题(例如ANDDESC等),但白名单似乎是这种情况下唯一的方法。

    更新

    尽管关于SQL注入保护的最佳实践达成了普遍一致,但仍存在许多不好的做法。 其中一些根深蒂固在PHP用户的头脑中。 例如,在这个页面上(尽管大多数访问者不可见) 有80多个被删除的答案 - 由于质量差或者推广不好和过时的做法而被社区删除。 更糟糕的是,一些不好的答案并未被删除,而是繁荣起来。

    例如,(1)是(2)仍然(3)很多(4)答案(5),其中包括第二个最有回报的答案,表明您手动字符串转义 - 一种过时的方法被证明是不安全的。

    或者有一个稍微好一点的答案,暗示了另一种字符串格式化方法,甚至认为它是最终的灵丹妙药。 当然,事实并非如此。 这种方法并不比常规字符串格式更好,但它保留了它的所有缺点:它仅适用于字符串,并且与其他任何手动格式一样,它本质上是可选的,非强制性度量,容易出现任何类型的人为错误。

    我认为所有这一切都是因为一个非常古老的迷信,得到OWASP或PHP手册等权威机构的支持,该手册宣称无论是“逃避”还是防止SQL注入都是平等的。

    无论PHP手册用了多*_escape_string*_escape_string绝不会使数据安全,并且从未打算过。 除了对字符串以外的任何SQL部分无用,手动转义是错误的,因为它是手动的,与自动化相反。

    而且OWASP更加糟糕,强调逃避用户输入,这完全是无稽之谈:在注入保护的背景下不应该有这样的话。 每个变量都有潜在的危险 - 无论来源如何! 或者换句话说,每一个变量都必须正确格式化以便放入查询中 - 不管源再次如何。 这是重要的目的地。 当开发人员开始将山羊与山羊分开时(考虑某些特定变量是否“安全”或不),他迈出了第一步走向灾难。 更不用说,即使措辞表明批量在入口处逃脱,类似于非常神奇的报价功能 - 已经被鄙视,被弃用并被删除。

    因此,与“转义”不同,准备好的语句是确实可以防止SQL注入的措施(适用时)。

    如果你还不确定,下面是我写的一步一步的解释,即SQL注入预防的Hitchhiker指南,我详细解释了所有这些问题,甚至编写了一个完全致力于不良实践和其披露的部分。

    链接地址: http://www.djcxy.com/p/331.html

    上一篇: How can I prevent SQL injection in PHP?

    下一篇: Close/hide the Android Soft Keyboard