【问题标题】:Why do we need to specify the parameter type in bindParam()?为什么我们需要在 bindParam() 中指定参数类型?
【发布时间】:2013-05-28 16:19:01
【问题描述】:

我有点困惑,为什么我们需要指定我们在 Php.PDO 中的 bindParam() 函数中传递的数据类型。例如这个查询:

$calories = 150; 
$colour = 'red';
$sth = $dbh->prepare('SELECT name, colour, calories
FROM fruit
WHERE calories < ? AND colour = ?');
$sth->bindParam(1, $calories, PDO::PARAM_INT); 
$sth->bindParam(2, $colour, PDO::PARAM_STR, 12);
$sth->execute();

如果我不指定第三个参数是否存在安全风险。我的意思是如果我只是在 bindParam() 中做:

$sth->bindParam(1, $calories); 
$sth->bindParam(2, $colour);

【问题讨论】:

    标签: php pdo sql-injection


    【解决方案1】:

    我知道这是一个老问题,但我想深入研究一下,因为我在绑定值/参数时总是忘记数据类型参数的内部工作原理。此外,我对 MySQL 有更多的经验,所以这个答案将更多地与 MySQL 相关。

    MySQL 会根据需要自动将数字转换为字符串,反之亦然,所以如果您这样做:

    SELECT * FROM tablename WHERE columnname = 42
    

    SELECT * FROM tablename WHERE columnname = '42'
    

    MySQL 知道 columnname 应该是什么列类型,如果提供了错误的类型作为值,它将自动进行类型转换。

    现在,看看the source code for PDOStatement

    if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_STR && param->max_value_len <= 0 && !Z_ISNULL_P(parameter)) {
        if (Z_TYPE_P(parameter) == IS_DOUBLE) {
            char *p;
            int len = zend_spprintf_unchecked(&p, 0, "%.*H", (int) EG(precision), Z_DVAL_P(parameter));
            ZVAL_STRINGL(parameter, p, len);
            efree(p);
        } else {
            convert_to_string(parameter);
        }
    } else if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_INT && (Z_TYPE_P(parameter) == IS_FALSE || Z_TYPE_P(parameter) == IS_TRUE)) {
        convert_to_long(parameter);
    } else if (PDO_PARAM_TYPE(param->param_type) == PDO_PARAM_BOOL && Z_TYPE_P(parameter) == IS_LONG) {
        convert_to_boolean(parameter);
    }
    

    您会从上面的代码中注意到,当绑定语句的第三个参数被省略或PDO_PARAM_STR(默认值)时,PHP 会将值类型转换为字符串。当它以PDO_PARAM_INT 提供时,与您实际期望的不同,它确实 将类型转换为整数,而是 PHP 将其转换为 long!所以如果你这样做:

    $stmt->bindValue("integer_column", "41.9", PDO_PARAM_INT);
    

    数据库实际上会收到长值41.9 以用于查询,然后将其四舍五入(至少由 MySQL)到最接近的整数,因此在上面对INTEGER 类型的列的查询将是42

    mysql> SELECT CAST(41.9 AS UNSIGNED);
    +------------------------+
    | CAST(41.9 AS UNSIGNED) |
    +------------------------+
    |                     42 |
    +------------------------+
    

    如果你有一个非数字字符串也是如此,例如"41.9asdf":

    $sth->bindValue("integer_column_value", "41.9asdf", PDO_PARAM_INT);
    

    它将首先由 PHP (41.9) 类型转换为 long,然后该值将由 MySQL (42) 四舍五入为整数。

    就像我之前提到的,MySQL 会自动将字符串转换为数字,所以如果你给 MySQL 字符串"123",那么将它转换为整数或任何其他数字类型都没有问题。但是,请记住,当 MySQL 从字符串转换为整数时,它会截断而不是舍入,因此这可能是绑定时提供类型的实际区别。

    mysql> SELECT CAST('123.6' AS UNSIGNED);
    +---------------------------+
    | CAST('123.6' AS UNSIGNED) |
    +---------------------------+
    |                       123 |
    +---------------------------+
    
    mysql> SELECT CAST(123.6 AS UNSIGNED);
    +-------------------------+
    | CAST(123.6 AS UNSIGNED) |
    +-------------------------+
    |                     124 |
    +-------------------------+
    

    以下是 PHP 中整数列将包含的值:

    $stmt->bindValue("integer_column", "123.6", PDO_PARAM_INT); // 124
    $stmt->bindValue("integer_column", "123.6"); // 123
    $stmt->bindValue("integer_column", (int) "123.6"); // 123
    $stmt->bindValue("integer_column", round("123.6")); // 124
    

    请注意,您可能会从上面注意到,PHP 将字符串转换为整数与 MySQL 不同。 MySQL 舍入浮点数,但 PHP 将使用 floor 值:

    var_dump((int) "123.6"); // int(123)
    var_dump((int) 123.6); // int(123)
    

    现在,要实际回答您问题的安全方面,将第三个参数提供给准备好的语句没有任何安全优势。准备好的语句本身足以在正确完成时减轻 SQL 注入(请参阅this link for some obscure edge-cases)。

    【讨论】:

      【解决方案2】:

      如果不指定类型,则默认将参数绑定为字符串。然后,您可能会得到一个相当于以下内容的查询:

      SELECT foo FROM bar WHERE baz = '42'
      

      这可能是也可能不是问题。就像 PHP 和其他编程语言一样,数据库具有 types 和类型之间的 casting 规则。 MySQL 通常会根据需要将数字字符串隐式转换为数字。一些数据库比其他数据库更严格,某些操作需要正确的类型。在大多数情况下,它几乎没有什么区别。但如果你真的需要将 42 传递为 42 而不是 '42',则需要将其显式绑定为 INT

      【讨论】:

        【解决方案3】:

        bindParam() 与类型一起使用可能被认为更安全,因为它允许更严格的验证,进一步防止 SQL 注入。但是,如果您不这样做,我不会说存在真正的安全风险,因为您使用prepared statement 来防止 SQL 注入比类型验证。实现这一点的更简单方法是简单地将数组传递给execute() 函数,而不是使用bindParam(),如下所示:

        $calories = 150; 
        $colour = 'red';
        
        $sth = $dbh->prepare('SELECT name, colour, calories
                              FROM fruit
                              WHERE calories < :calories AND colour = :colour');
        
        $sth->execute(array(
            'calories' => $calories,
            'colour' => $colour
        ));
        

        您没有义务使用字典,您也可以像使用问号一样进行操作,然后将其以相同的顺序放入数组中。但是,即使这很有效,我还是建议养成使用第一个的习惯,因为一旦达到一定数量的参数,这种方法就会变得一团糟。为了完整起见,如下所示:

        $calories = 150; 
        $colour = 'red';
        
        $sth = $dbh->prepare('SELECT name, colour, calories
                              FROM fruit
                              WHERE calories < ? AND colour = ?');
        
        $sth->execute(array($calories, $colour));
        

        【讨论】:

        • 在旧的mysql_x函数中,如果我们使用$id而不做(int)$id;即使我们使用了 mysql_real_escape_string($id) 我们仍然可以得到 SQL 注入。但是在 PDO 中也一样吗?
        • Prepared statements 通过准备查询服务器端然后将参数绑定到它来帮助避免 MySQL 注入。你的做法没有错,我只是向你展示了一种更简单的做法,至少在我看来是这样。
        • 我理解这两种方式都是正确的。我想知道的是,例如,如果攻击者提供 id 的值,例如:$_POST['id']='1 或 3=3'。这将永远是正确的,但如果我们这样做: $id=(int)$id;上述攻击不可能发生。所以我想知道的是,在 PDO 中我可以将 $_POST['id'] 放在 bindParam() 函数中吗?没有指定“id”是一个整数?
        • 正如我上面所说,这是避免 SQL 注入的准备好的语句的全部要点。将参数绑定到查询的方式并不重要。可以肯定的是,我尝试在我的网站上注入我的登录表单,我修改为使用bindParam() 而不指定类型。它没有用,所以即使你没有指定类型,你也是安全的:)
        • 谢谢,我也在本地进行了一些测试,是的,注入不起作用。谢谢!
        猜你喜欢
        • 2013-10-18
        • 2020-12-17
        • 1970-01-01
        • 2011-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-10-23
        相关资源
        最近更新 更多