【问题标题】:Reference: What is a perfect code sample using the MySQL extension? [closed]参考:什么是使用 MySQL 扩展的完美代码示例? [关闭]
【发布时间】:2011-09-06 02:05:38
【问题描述】:

这是为了创建一个社区学习资源。目标是提供不会重复在复制/粘贴 PHP 代码中经常出现的可怕错误的良好代码示例。我已请求将其制作为社区 Wiki。

并不是一场编码竞赛。这不是为了找到最快或最紧凑的查询方式 - 它是为了提供一个很好的、易读的参考资料,尤其是对于新手。

每天,使用 Stack Overflow 上的 mysql_* 系列函数的 非常糟糕 代码 sn-ps 的问题大量涌入。虽然通常最好将这些人引向 PDO,但有时这既不可能(例如继承的遗留软件)也不是现实的期望(用户已经在他们的项目中使用它)。

使用mysql_* 库的代码的常见问题包括:

  • 值中的 SQL 注入
  • LIMIT 子句和动态表名中的 SQL 注入
  • 没有错误报告(“为什么这个查询不起作用?”)
  • 错误报告中断(即即使代码投入生产,也总是会出现错误)
  • 值输出中的跨站点脚本 (XSS) 注入

让我们编写一个 PHP 代码示例,使用 mySQL_* family of functions 执行以下操作:

  • 接受两个 POST 值,id(数字)和 name(字符串)
  • 对表 tablename 执行 UPDATE 查询,更改 ID 为 id 的行中的 name
  • 失败时,优雅退出,但仅在生产模式下显示详细错误。 trigger_error() 就足够了;或者使用您选择的方法
  • 输出消息“$name更新。”

并且没有显示出上面列出的任何弱点。

应该尽可能简单。理想情况下,它不包含任何函数或类。目标不是创建一个可复制/可粘贴的库,而是展示为确保数据库查询安全而需要做的最少工作。

优秀 cmets 的奖励积分。

我们的目标是让这个问题成为用户在遇到问题代码错误(即使它根本不是问题的焦点)或遇到失败的查询时可以链接到的资源,并且不知道怎么解决。

抢占 PDO 讨论:

是的,通常最好将写这些问题的个人指导给 PDO。当它是一个选项时,我们应该这样做。然而,这并不总是可能的——有时,提问者正在处理遗留代码,或者已经在这个库中取得了长足的进步,现在不太可能改变它。此外,如果使用得当,mysql_* 系列函数是完全安全的。所以请不要在这里回答“使用 PDO”。

【问题讨论】:

    标签: php mysql security sql-injection


    【解决方案1】:

    我的尝试。试图让它尽可能简单,同时仍然保持一些现实世界的便利。

    处理 unicode 并使用松散比较以提高可读性。做个好人;-)

    <?php
    
    header('Content-type: text/html; charset=utf-8');
    error_reporting(E_ALL | E_STRICT);
    ini_set('display_errors', 1);
    // display_errors can be changed to 0 in production mode to
    // suppress PHP's error messages
    
    /*
    Can be used for testing
    $_POST['id'] = 1;
    $_POST['name'] = 'Markus';
    */
    
    $config = array(
        'host' => '127.0.0.1', 
        'user' => 'my_user', 
        'pass' => 'my_pass', 
        'db' => 'my_database'
    );
    
    # Connect and disable mysql error output
    $connection = @mysql_connect($config['host'], 
        $config['user'], $config['pass']);
    
    if (!$connection) {
        trigger_error('Unable to connect to database: ' 
            . mysql_error(), E_USER_ERROR);
    }
    
    if (!mysql_select_db($config['db'])) {
        trigger_error('Unable to select db: ' . mysql_error(), 
            E_USER_ERROR);
    }
    
    if (!mysql_set_charset('utf8')) {
        trigger_error('Unable to set charset for db connection: ' 
            . mysql_error(), E_USER_ERROR);
    }
    
    $result = mysql_query(
        'UPDATE tablename SET name = "' 
        . mysql_real_escape_string($_POST['name']) 
        . '" WHERE id = "' 
        . mysql_real_escape_string($_POST['id']) . '"'
    );
    
    if ($result) {
        echo htmlentities($_POST['name'], ENT_COMPAT, 'utf-8') 
            . ' updated.';
    } else {
        trigger_error('Unable to update db: ' 
            . mysql_error(), E_USER_ERROR);
    }
    

    【讨论】:

    • @Pekka Aw 废话,修复它。谢谢!
    • -1 for #error_reporting(~E_ALL); ... to disable error output - 不好的建议。不能再投反对票了,否则我会为 @ 运营商 -1。
    • @OZ_ 我明白你关于@ 的观点,但是为什么在生产模式下关闭错误报告不好?
    • @OZ_:为什么这是个问题?在生产模式下,禁用错误输出并没有什么坏处,只要您将其记录在其他地方即可。使用 @ 是隐藏来自 mysql_connect 的讨厌输出的唯一方法。请重新考虑。
    • @Pekka, @Znarkus, error_reporting(0); 这是一个非常糟糕的建议,因为错误处理程序并不总是会处理错误,因为这个处理程序并不总是会被编码。 ini_set('display_errors',0) - 此变体可用于生产代码,隐藏错误文本,并且仅在处理错误时使用。
    【解决方案2】:

    我决定急于求成,放点东西。这是开始的事情。出错时抛出异常。

    function executeQuery($query, $args) {
        $cleaned = array_map('mysql_real_escape_string', $args);
    
        if($result = mysql_query(vsprintf($query, $cleaned))) {
            return $result;
        } else {
            throw new Exception('MySQL Query Error: ' . mysql_error());
        }
    }
    
    function updateTablenameName($id, $name) {
        $query = "UPDATE tablename SET name = '%s' WHERE id = %d";
    
        return executeQuery($query, array($name, $id));
    }
    
    try {
        updateTablenameName($_POST['id'], $_POST['name']);
    } catch(Exception $e) {
        echo $e->getMessage();
        exit();
    }
    

    【讨论】:

    • 虽然可行,但对于新手和复制粘贴人员来说太复杂了。
    • @Aaron - 你读过问题和 cmets 吗?正确阅读它们,只是复制粘贴不是这个问题的解决方案。
    • 关于:失败时,优雅退出,但在生产模式下显示详细错误。也许在echo $e-&gt;getMessage();周围添加一些条件?
    • @Aaron - 我不想为您的回答提及它,而是希望其他人也能效仿。我能理解你自己的答案。
    • Err,这不是将 $id 值分配给 name 并将 $name 值分配给 id 吗?除了您在调用函数时调用 updateTableName 的事实 updateTablenameName :p。
    【解决方案3】:
    /**
     * Rule #0: never trust users input!
     */
    
    //sanitize integer value
    $id = intval($_GET['id']);
    //sanitize string value;
    $name = mysql_real_escape_string($_POST['name']);
    //1. using `dbname`. is better than using mysql_select_db()
    //2. names of tables and columns should be quoted by "`" symbol
    //3. each variable should be sanitized (even in LIMIT clause)
    $q = mysql_query("UPDATE `dbname`.`tablename` SET `name`='".$name."' WHERE `id`='".$id."' LIMIT 0,1 ");
    if ($q===false)
    {
        trigger_error('Error in query: '.mysql_error(), E_USER_WARNING);
    }
    else
    {
        //be careful! $name contains user's data, remember Rule #0
        //always use htmlspecialchars() to sanitize user's data in output
        print htmlspecialchars($name).' updated';
    }
    
    ########################################################################
    //Example, how easily is to use set_error_handler() and trigger_error()
    //to control error reporting in production and dev-code
    //Do NOT use error_reporting(0) or error_reporting(~E_ALL) - each error
    //should be fixed, not muted
    function err_handler($errno, $errstr, $errfile, $errline)
    {
        $hanle_errors_print = E_ALL & ~E_NOTICE;
    
        //if we want to print this type of errors (other types we can just write in log-file)
        if ($errno & $hanle_errors_print)
        {
            //$errstr can contain user's data, so... Rule #0
            print PHP_EOL.'Error ['.$errno.'] in file '.$errfile.' in line '.$errline
                  .': '.htmlspecialchars($errstr).PHP_EOL;
        }
        //here you can write error into log-file
    }
    
    set_error_handler('err_handler', E_ALL & ~E_NOTICE & E_USER_NOTICE & ~E_STRICT & ~E_DEPRECATED);
    

    以及对cmets的一些解释:

    //1. using `dbname`. is better than using mysql_select_db()
    

    使用 mysql_select_db 可以创建错误,但要查找和修复它们并不容易。
    例如,在某些脚本中您将 db1 设置为数据库,但在某些函数中您需要将 db2 设置为数据库。
    调用该函数后会切换数据库,脚本中的所有后续查询都将被破坏或破坏错误数据库中的某些数据(如果表名和列名重合)。

    //2. names of tables and columns should be quoted by "`" symbol 
    

    某些列名也可以是 SQL 关键字,使用“`”符号会有所帮助。
    此外,插入到查询中的所有字符串值都应该用 ' 符号引用。

    //always use htmlspecialchars() to sanitize user's data in output
    它将帮助您防止XSS-attacks

    【讨论】:

    • 不错,我喜欢!我也喜欢错误处理程序,但对于新手来说,在这里掌握它可能太多了 - 现在可以选择将其删除,还是将其作为“可选”移动到底部? (我把mysqli中的i删掉了)
    • 随意编辑我蹩脚的英语中的错误:)
    • @Pekka 我将尝试更多地关注 mysql_,现在将进行编辑。
    • 我会将字符串连接替换为sprintf。这使得内容更具可读性,并且可能的语法错误(缺少引号和内容)更加明显。
    • @Yoshi 我觉得是个人喜好,肯定不会更简单。
    【解决方案4】:
    <?  
    mysql_connect(); 
    mysql_select_db("new"); 
    $table = "test"; 
    if($_SERVER['REQUEST_METHOD']=='POST') {
      $name = mysql_real_escape_string($_POST['name']); 
      if ($id = intval($_POST['id'])) { 
        $query="UPDATE $table SET name='$name' WHERE id=$id"; 
      } else { 
        $query="INSERT INTO $table SET name='$name'"; 
      } 
      mysql_query($query) or trigger_error(mysql_error()." in ".$query); 
      header("Location: http://".$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF']);  
      exit;  
    }  
    if (!isset($_GET['id'])) {
      $LIST=array(); 
      $query="SELECT * FROM $table";  
      $res=mysql_query($query); 
      while($row=mysql_fetch_assoc($res)) $LIST[]=$row; 
      include 'list.php'; 
    } else {
      if ($id=intval($_GET['id'])) { 
        $query="SELECT * FROM $table WHERE id=$id";  
        $res=mysql_query($query); 
        $row=mysql_fetch_assoc($res); 
        foreach ($row as $k => $v) $row[$k]=htmlspecialchars($v); 
      } else { 
        $row['name']=''; 
        $row['id']=0; 
      } 
      include 'form.php'; 
    }  
    ?>
    

    form.php

    <? include 'tpl_top.php' ?>
    <form method="POST">
    <input type="text" name="name" value="<?=$row['name']?>"><br>
    <input type="hidden" name="id" value="<?=$row['id']?>">
    <input type="submit"><br>
    <a href="?">Return to the list</a>
    </form>
    <? include 'tpl_bottom.php' ?>
    

    list.php

    <? include 'tpl_top.php' ?>
    <a href="?id=0">Add item</a>
    <? foreach ($LIST as $row): ?>
    <li><a href="?id=<?=$row['id']?>"><?=$row['name']?></a>
    <? endforeach ?>
    <? include 'tpl_bottom.php' ?>
    

    【讨论】:

      【解决方案5】:

      看起来我的其他答案没有达到问题的目的。
      (这个也不满足一些要求,但可以看出,如果不实现处理占位符的功能,就无法实现安全的解决方案,占位符是安全查询的基石)

      因此,这是另一个尝试发布简洁的解决方案以使 mysql 查询安全且方便。

      我很久以前编写的一个函数,在我转向基于公司标准 OOP 的解决方案之前,它对我很有用。
      有 2 个目标要追求:安全易用性

      第一个通过实现占位符实现。
      第二个是通过实现占位符和不同的结果类型来实现的。

      功能肯定不理想。一些缺点是:

      • 没有% 字符必须直接放在查询中,因为它使用 printf 语法。
      • 不支持多个连接。
      • 没有标识符的占位符(以及许多其他方便的占位符)。
      • 同样,没有标识符占位符!"ORDER BY $field" 案件需人工处理!
      • 当然,OOP 实现会更加灵活,具有简洁的不同方法,而不是丑陋的“模式”变量以及其他必要的方法。

      但它很好,安全简洁,无需安装整个库。

      function dbget() {
        /*
        usage: dbget($mode, $query, $param1, $param2,...);
        $mode - "dimension" of result:
        0 - resource
        1 - scalar
        2 - row
        3 - array of rows
        */
        $args = func_get_args();
        if (count($args) < 2) {
          trigger_error("dbget: too few arguments");
          return false;
        }
        $mode  = array_shift($args);
        $query = array_shift($args);
        $query = str_replace("%s","'%s'",$query); 
      
        foreach ($args as $key => $val) {
          $args[$key] = mysql_real_escape_string($val);
        }
      
        $query = vsprintf($query, $args);
        if (!$query) return false;
      
        $res = mysql_query($query);
        if (!$res) {
          trigger_error("dbget: ".mysql_error()." in ".$query);
          return false;
        }
      
        if ($mode === 0) return $res;
      
        if ($mode === 1) {
          if ($row = mysql_fetch_row($res)) return $row[0];
          else return NULL;
        }
      
        $a = array();
        if ($mode === 2) {
          if ($row = mysql_fetch_assoc($res)) return $row;
        }
        if ($mode === 3) {
          while($row = mysql_fetch_assoc($res)) $a[]=$row;
        }
        return $a;
      }
      ?>
      

      使用示例

      $name = dbget(1,"SELECT name FROM users WHERE id=%d",$_GET['id']);
      $news = dbget(3,"SELECT * FROM news WHERE title LIKE %s LIMIT %d,%d",
                    "%$_GET[search]%",$start,$per_page);
      

      从上面的例子可以看出,与Stackoverflow中发布的所有代码的主要区别在于,安全和数据检索例程都封装在函数代码中。因此,无需手动绑定、转义/引用或强制转换,也无需手动数据检索。

      结合其他辅助功能

      function dbSet($fields,$source=array()) {
        $set = '';
        if (!$source) $source = &$_POST;
        foreach ($fields as $field) {
          if (isset($source[$field])) {
            $set.="`$field`='".mysql_real_escape_string($source[$field])."', ";
          }
        }
        return substr($set, 0, -2); 
      }
      

      这样使用

      $fields = explode(" ","name surname lastname address zip phone regdate");
      $_POST['regdate'] = $_POST['y']."-".$_POST['m']."-".$_POST['d'];
      $sql = "UPDATE $table SET ".dbSet($fields).", stamp=NOW() WHERE id=%d";
      $res = dbget(0,$sql, $_POST['id']);
      if (!$res) {
        _503;//calling generic 503 error function
      }
      

      它可能涵盖几乎所有需求,包括来自 OP 的示例案例。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-06-06
        • 1970-01-01
        • 1970-01-01
        • 2023-02-15
        • 2023-04-05
        相关资源
        最近更新 更多