如何动态准备 SQL 查询(也包括列名)避免 SQL 注入

Posted

技术标签:

【中文标题】如何动态准备 SQL 查询(也包括列名)避免 SQL 注入【英文标题】:How to prepare SQL query dynamically (column names too) avoiding SQL injection 【发布时间】:2019-08-25 13:27:28 【问题描述】:

我最近使用prepare()bind_param() 了解了SQL 注入和避免它的php 建议。 现在,我想动态准备 SQL 查询,同时添加列名和值。

我以前是这样做的,html 输入的name 字段与 mysql 数据库列的名称相同。

    <input type="text" name="firstname" >
    <input type="text" name="lastname" >

然后,使用 mysqli 动态创建 SQL 查询。

    // Extract values from POST
    $parameters = $_POST;
    // Organize the values in two strings
    foreach ($parameters as $id => $value) 
        $fields = $fields . "`" . $id . "`,";
        $values = $values . "'" . $value . "',"; 

        /*e.g.
            $fields = `firstname`,`lastname`
            $values = 'John','Wick'
        */
    

    // Write into the database
    $sql = "INSERT INTO `user` ($fields) VALUES ($values)";

    /*e.g.
        INSERT INTO `user` (`firstname`,`lastname`) VALUES ('John','Wick')
    */

我想知道是否有办法使用prepare()bind_param() 来避免SQL 注入,可能是在HTML 输入标记中添加一些data-type="s",或者是否有更好的、更多的最佳实践,方法。

【问题讨论】:

【参考方案1】:

您只能将绑定参数用于常量值元素——带引号的字符串、带引号的日期时间或数字文字。

您不能将参数占位符用于 SQL 中的任何其他内容,例如列名、表名、值列表、SQL 关键字或表达式或其他语法。

如果您需要使列名动态化,唯一的选择是根据已知列的列表来验证它们。

$columns_in_user_table = [
  'userid'=>null,
  'username'=>'',
  'firstname'=>'',
  'lastname'=>''
];
// Extract values from POST, but only those that match known columns
$parameters = array_intersect_key($_POST, $columns_in_user_table);
// Make sure no columns are missing; assign default values as needed
$parameters = array_merge($columns_in_user_table, $parameters);

如果你使用 PDO 而不是 mysqli,你可以跳过绑定。只需使用命名参数,并将列值对的关联数组直接传递给execute()

$columns = [];
$placeholders = [];
foreach ($parameters as $col => $value) 
    $columns[] = "`$col`";
    $placeholders[] = ":$col";

$column_list = implode($columns, ',');
$placeholder_list = implode($placeholders, ',');

// Write into the database
$sql = "INSERT INTO `user` ($column_list) VALUES ($placeholder_list)";

$stmt = $pdo->prepare($sql);
$stmt->execute($parameters);

【讨论】:

谢谢!这就是我需要的。我将切换到 PDO。我有一个疑问:当我们在 :$col 行中使用名称参数时,是否要求 $parameters 数组具有 array(':firstname' =&gt; 'John',':lastname' =&gt; 'Wick') 的形式,在 col 名称之前有冒号? 多年前需要绑定参数键中的冒号,但我认为有人意识到没有理由这样做,并且将 $_POST 数组直接转换为所需的参数变得更加工作。我忘记了哪个版本的 PHP 对其进行了更改,但只要您不使用 PHP 的终止版本,您就不需要在绑定名称中放置冒号。它们仅在 SQL 查询中是必需的。【参考方案2】:

我注意到您在问题中包含了 mysqli 标签,因此假设您的数据库是 MySQL 并且您使用的是本机 MySQL 函数,那么您可以执行以下操作:

$stmt = mysqli_prepare($link, "INSERT INTO CountryLanguage VALUES (?, ?, ?, ?)");
mysqli_stmt_bind_param($stmt, 'sssd', $code, $language, $official, $percent);

$code = 'DEU';
$language = 'Bavarian';
$official = "F";
$percent = 11.2;

/* execute prepared statement */
mysqli_stmt_execute($stmt);

是的,我直接从PHP manual page on mysqli_stmt_bind_param. 中撕下了它

【讨论】:

感谢您的快速答复!但是,我想创建 SQL 语句,添加列名,如CountryLanguage,所需数量的? 符号和数据类型字符串,如'sssd',使用循环或函数而不是创建如您的示例中那样以静态方式准备查询。

以上是关于如何动态准备 SQL 查询(也包括列名)避免 SQL 注入的主要内容,如果未能解决你的问题,请参考以下文章

如何从 SQL 表中动态获取单元格值?

如何MyBatis中使用动态SQL查询与注释

sq优化的几种方法(转)

如何在 django 的插入查询中动态使用列名

在python mysql查询中动态地传递列名和值。

ExecuteNonQuery,ExecuteReader,ExecuteScalar 区别