如果我对整数使用准备好的语句,我是否需要进行更多检查?

信息安全 php sql注入
2021-08-18 09:52:30

我目前正在使用 PDO 和一些整数值的准备好的语句(请参阅PDO::PARAM_INT)。这意味着我这样称呼 PDO:

$stmt = $conn->prepare("SELECT `lastMove` FROM ".GAMES_TABLE.
                " WHERE `id`=:gameID AND (`whiteUserID` = ".USER_ID.
                " OR `blackUserID` = ".USER_ID.") LIMIT 1");
$stmt->bindValue(':gameID', $_POST['gameID'], PDO::PARAM_INT);
$stmt->execute();

我需要做任何检查$_POST['gameID']吗?可能存在哪些漏洞?

PDO 和整数

Krzysztof Kotowicz 发表了很好的评论。他说 PDO 将整数视为字符串。所以我启用了 MySQL general_log:

  1. 打开 /etc/mysql/my.cnf
  2. 设置general_log为 1 并将路径添加到general_log_file(例如/var/log/mysql/mysql.log
  3. 重启你的 MySQL 服务器: sudo /etc/init.d/mysql restart
  4. 看一下文件: tail -f /var/log/mysql/mysql.log
  5. 调用以下代码段:

测试.php:

$stmt = $conn->prepare("SELECT `lastMove` FROM ".GAMES_TABLE.
                " WHERE `id`=:gameID AND (`whiteUserID` = :uid".
                " OR `blackUserID` = :uid) LIMIT 1");
$stmt->bindValue(':gameID', "abc", PDO::PARAM_INT);
$stmt->bindValue(':uid', "1'", PDO::PARAM_INT);
$stmt->execute();
$result = $stmt->fetch(PDO::FETCH_ASSOC);

这是结果:

111114 17:32:54   241 Connect   chessuser@localhost on chess
          241 Query SELECT `user_id` FROM `chess_users` WHERE `user_id`='1'  LIMIT 1
          241 Query SELECT `lastMove` FROM chess_games WHERE `id`='abc' AND (`whiteUserID` = '1\'' OR `blackUserID` = '1\'') LIMIT 1
          241 Quit  

所以很明显,它没有在 PHP 端成为 int (PHP 版本 5.3.2-1ubuntu4.10)

1个回答

首先:您没有充分利用准备好的语句。这可能会使您面临 SQL 注入问题。

您不应该使用字符串连接来构建$conn->prepare(). 相反,您应该使用字符串常量作为 SQL 查询,并依赖于bindValue()填充动态值。在一个理想的世界里,你可以这样写:

$stmt = $conn->prepare("SELECT `lastMove` FROM :table".
            " WHERE `id`=:gameID AND (`whiteUserID` = :userid".
            " OR `blackUserID` = :userid) LIMIT 1");
$stmt->bindValue(':gameID', $_POST['gameID'], PDO::PARAM_INT);
$stmt->bindValue(':table', GAMES_TABLE, PDO::PARAM_STR);
$stmt->bindValue(':userid', USER_ID, PDO::PARAM_STR);
$stmt->execute();

但是,PDO 准备好的语句不允许您以这种方式指定表名,因此您不得不对表名使用字符串连接,如下所示:

$stmt = $conn->prepare("SELECT `lastMove` FROM ".
            mysql_real_escape_string(GAMES_TABLE).
            " WHERE `id`=:gameID AND (`whiteUserID` = :userid".
            " OR `blackUserID` = :userid) LIMIT 1");
$stmt->bindValue(':gameID', $_POST['gameID'], PDO::PARAM_INT);
$stmt->bindValue(':userid', USER_ID, PDO::PARAM_STR);
$stmt->execute();

使用字符串连接与任何非常量字符串来构建第一个参数会prepare()引入 SQL 注入攻击的危险,因此您需要在这里小心。您需要仔细检查这GAMES_TABLE是一个常数,并且永远不会受到攻击者的影响(您可以考虑将其内联)。此外,您可能想要清理/转义它以防万一,如我上面显示的代码所示。这是准备好的语句的一个令人讨厌的限制,他们无法为您处理这个问题。

经验法则:准备好的语句不会受到 SQL 注入的影响,只要 SQL 查询本身是一个常量字符串。

不幸的是,您可能会发现一些准备好的语句过于受限的情况,例如动态 ORDER BY 或 LIMIT 参数以及动态表名,如上所示。对于这些情况,您可能需要使用字符串连接。在这些情况下,您必须非常小心,因为使用字符串连接会重新引入 SQL 注入漏洞的风险。

在任何情况下,您仍然应该尽可能使用准备好的语句,例如 for USER_ID,因为它比自己进行字符串连接更不容易出错。此外,因为准备好的语句可以更容易地验证您是否避免了 SQL 注入缺陷,所以尽可能使用准备好的语句而不是字符串连接对您有利。

附加检查:要回答您最初的问题,如果 PDO 正确实现了其 API,则不需要对整数值进行任何其他检查以防止 SQL 注入问题。不幸的是,@Krzysztof Kotowicz 指出 PHP PDO 库有问题:它忽略了PDO::PARAM_INT参数并将所有内容都视为字符串,无论如何。因此,您可能应该进行额外的编码来弥补 PDO 库中的这个缺陷。特别是,您应该在传递它之前将值强制(强制转换)为整数。因此,我建议如下:

// don't use $_POST['gameID'] elsewhere; use $gameID
$gameID = (int) $_POST['gameID'];

$stmt = $conn->prepare("SELECT `lastMove` FROM ".
            mysql_real_escape_string(GAMES_TABLE).
            " WHERE `id`=:gameID AND (`whiteUserID` = :userid".
            " OR `blackUserID` = :userid) LIMIT 1");
$stmt->bindValue(':gameID', $gameID, PDO::PARAM_INT);
$stmt->bindValue(':userid', USER_ID, PDO::PARAM_STR);
$stmt->execute();

另外,您需要确保,如果攻击者可以猜到有效的游戏 ID,它不会帮助攻击者绕过您代码中的任何访问控制检查。参见,例如,OWASP 的不安全的直接对象引用漏洞类别。

对于字符串值,清理数据以确保它匹配一些适合该值的正则表达式也可能是谨慎的。