为什么不应该在PHP中使用mysql_*函数?

2714

为什么不应该使用mysql_*函数?(例如mysql_query()mysql_connect()mysql_real_escape_string())有哪些技术原因?

即使它们在我的网站上运行良好,为什么我还应该使用其他内容?

如果它们不能在我的网站上运行,为什么会出现如下错误:

警告:mysql_connect():没有这个文件或目录


3
错误可能是这样的:致命错误:未捕获的错误:调用未定义的函数mysql_connect()... - Bimal Poudel
47
"Deprecated"这个词已经足够理由来避免使用它们。 - Sasa1234
14个回答

2230

MySQL扩展:

  • 目前不再进行积极开发
  • 自PHP 5.5(发布于2013年6月)起,官方弃用
  • 完全删除自PHP 7.0(发布于2015年12月)
    • 这意味着自2018年12月31日以来,在任何受支持的PHP版本中都不存在它。如果您正在使用支持它的PHP版本,则使用的是一个没有修复安全问题的版本。
  • 缺乏面向对象的接口
  • 不支持:
    • 非阻塞式异步查询
    • 预处理语句或参数化查询
    • 存储过程
    • 多个语句
    • 事务
    • “新”的密码身份验证方法(在MySQL 5.6中默认启用;在5.7中必需)
    • MySQL 5.1或更高版本中的任何新功能

由于它已被弃用,使用它会使您的代码不够未来化。

缺乏对预处理语句的支持特别重要,因为它们提供了一种比手动使用单独的函数调用进行转义和引用外部数据更清晰、更少出错的方法。

请参见SQL扩展的比较


311
仅仅因为被弃用,就足以避免使用它们。它们终将不再存在,如果你依赖它们,你将不会感到愉快。其余部分只是一些例子,使用旧的扩展会阻止人们学习新的东西。 - Tim Post
123
弃用并不是每个人都认为的万能解决方法。PHP自己有一天也将被淘汰,但我们今天所拥有的工具是我们所依赖的。当我们必须更换工具时,我们会这样做。 - Lightness Races in Orbit
148
@LightnessRacesinOrbit — Deprecation不是万能的,它只是一个标志,表示“我们认识到这个东西很糟糕,所以我们不会再支持它太久了”。虽然为了更好地保护代码的未来而移除弃用功能是一个很好的理由,但它并不是唯一的理由(甚至不是主要的理由)。改变工具是因为有更好的工具可用,而不是因为你被迫这样做。(在你被迫之前改变工具意味着你不是仅仅因为代码已经停止工作需要马上修复才去学习新工具...而这是最糟糕的时间去学习新工具) - Quentin
对我来说,准备好的语句是最重要的。PHP早期在安全方面被诟病的很大一部分原因是由于早期结合魔术变量和SQL插值而产生了一些非常愚蠢的代码。准备好的语句可以很大程度上防止这种情况发生。永远不要插值SQL。就是这样...不要这么做。 - Shayne
不支持: 非阻塞异步查询 - 这也是不使用PDO的原因,它不支持异步查询(与mysqli不同) - hanshenrik

1361
PHP提供了三种不同的API来连接MySQL。这些是mysql(在PHP 7中已删除),mysqliPDO扩展。 mysql_*函数曾经非常流行,但现在不再鼓励使用。文档团队正在讨论数据库安全情况,并教育用户摆脱常用的ext/mysql扩展(请查看php.internals: deprecating ext/mysql)。
而后来的PHP开发团队决定在用户通过mysql_connect()、mysql_pconnect()或内置在ext/mysql中的隐式连接功能连接到MySQL时生成E_DEPRECATED错误。
ext/mysql在PHP 5.5中被正式弃用,并在PHP 7中被移除。
看到红框了吗?
当你访问任何mysql_*函数的手册页面时,你会看到一个红框,解释它不应再被使用。
为什么呢?
摆脱ext/mysql不仅仅是为了安全性,还为了能够使用MySQL数据库的所有功能。 ext/mysql是为MySQL 3.23构建的,自那时以来只有很少的添加,同时大部分保持与这个旧版本的兼容性,这使得代码更难维护。不受ext/mysql支持的缺失功能包括:(来自PHP手册)。 不使用mysql_*函数的原因
  • 不再进行主动开发
  • 自 PHP 7 起已被移除
  • 缺乏面向对象的接口
  • 不支持非阻塞的异步查询
  • 不支持预处理语句或参数化查询
  • 不支持存储过程
  • 不支持多条语句
  • 不支持事务
  • 不支持 MySQL 5.1 的所有功能

以上观点引用自Quentin的回答

特别重要的是,缺乏对预处理语句的支持,因为它们提供了比使用单独的函数调用手动转义和引用外部数据更清晰、更少出错的方法。

请参阅SQL扩展的比较


抑制弃用警告
在代码转换为MySQLi/PDO时,可以通过在php.ini中设置error_reporting来排除E_DEPRECATED错误,从而抑制警告。
error_reporting = E_ALL ^ E_DEPRECATED

请注意,这也会隐藏其他弃用警告,然而,这些警告可能与MySQL以外的事物有关。(来自PHP手册
文章PDO vs. MySQLi: 你应该选择哪个?Dejan Marjanovic撰写,将帮助你做出选择。
而更好的方法是使用PDO,我现在正在撰写一个简单的PDO教程。
一个简单而简短的PDO教程
Q. 我脑海中的第一个问题是:什么是`PDO`?
A. “PDO - PHP Data Objects - 是一个数据库访问层,提供了一种统一的访问多个数据库的方法。”

alt text


连接到MySQL

使用mysql_*函数或者我们可以说是老方法(在PHP 5.5及以上版本中已弃用)

$link = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('testdb', $link);
mysql_set_charset('UTF-8', $link);

使用PDO:你只需要创建一个新的PDO对象。构造函数接受参数来指定数据库源,PDO的构造函数通常接受四个参数,它们是DSN(数据源名称)和可选的用户名和密码。
在这里,我认为你对除了DSN之外的所有内容都很熟悉;这在PDO中是新的。DSN基本上是一个包含选项的字符串,告诉PDO要使用哪个驱动程序和连接详细信息。如需进一步参考,请查看PDO MySQL DSN
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'username', 'password');

注意:你也可以使用charset=UTF-8,但有时会导致错误,所以最好使用utf8。
如果有任何连接错误,它将抛出一个PDOException对象,可以捕获以进一步处理异常。
好好阅读:连接和连接管理 ¶ 你还可以将几个驱动选项作为数组传递给第四个参数。我建议传递参数,将PDO置于异常模式。因为某些PDO驱动程序不支持原生准备语句,所以PDO会模拟准备。它还允许你手动启用此模拟。要使用原生的服务器端准备语句,你应该明确将其设置为false。
另一种方法是关闭默认启用的MySQL驱动程序中的准备模拟,但为了安全地使用PDO,应该关闭准备模拟。
稍后我会解释为什么应该关闭准备模拟。要找到原因,请查看这篇文章
只有在使用旧版本的MySQL时才能使用,但我不推荐这样做。
以下是一个示例,说明如何操作:
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8', 
              'username', 
              'password',
              array(PDO::ATTR_EMULATE_PREPARES => false,
              PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));

我们可以在PDO构造之后设置属性吗?
是的,我们可以使用setAttribute方法在PDO构造之后设置一些属性。
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=UTF-8', 
              'username', 
              'password');
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);

错误处理
错误处理在PDO中比mysql_*要简单得多。
使用mysql_*时的常见做法是:
//Connected to MySQL
$result = mysql_query("SELECT * FROM table", $link) or die(mysql_error($link));

OR die()不是处理错误的好方法,因为我们无法在die中处理错误。它只会突然结束脚本,然后将错误信息输出到屏幕上,而这通常不是您想要展示给最终用户的,还会让可恶的黑客发现您的架构。相反,mysql_*函数的返回值通常可以与mysql_error()一起用来处理错误。

PDO提供了更好的解决方案:异常处理。我们在使用PDO时应该将其包装在try-catch块中。我们可以通过设置错误模式属性来强制PDO进入三种错误模式之一。以下是三种错误处理模式:

  • PDO::ERRMODE_SILENT。它只是设置错误代码,并且与mysql_*的作用基本相同,您必须检查每个结果,然后查看$db->errorInfo();以获取错误详细信息。
  • PDO::ERRMODE_WARNING引发E_WARNING。(运行时警告(非致命错误)。脚本的执行不会停止。)
  • PDO::ERRMODE_EXCEPTION:抛出异常。它表示由PDO引发的错误。您不应该从自己的代码中抛出PDOException。有关PHP中异常的更多信息,请参阅异常。当未被捕获时,它的作用非常类似于or die(mysql_error());。但与or die()不同的是,PDOException可以被捕获并进行优雅处理,如果您选择这样做的话。

好文章

就像:

$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
$stmt->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );

你可以像下面这样用try-catch来包裹它:
try {
    //Connect as appropriate as above
    $db->query('hi'); //Invalid query!
} 
catch (PDOException $ex) {
    echo "An Error occured!"; //User friendly message/message you want to show to user
    some_logging_function($ex->getMessage());
}

你现在不必处理try-catch。你可以在任何合适的时候捕获它,但我强烈建议你使用try-catch。此外,将其捕获在调用PDO相关内容的函数外部可能更有意义。
function data_fun($db) {
    $stmt = $db->query("SELECT * FROM table");
    return $stmt->fetchAll(PDO::FETCH_ASSOC);
}

//Then later
try {
    data_fun($db);
}
catch(PDOException $ex) {
    //Here you can handle error and show message/perform action you want.
}

此外,你可以使用or die()或者我们可以说像mysql_*这样的方式来处理,但是它们会有很大的差异。在生产环境中,你可以通过关闭display_errors并仅查看错误日志来隐藏危险的错误信息。
现在,在阅读了上面的所有内容之后,你可能会想:当我只想开始学习简单的SELECTINSERTUPDATEDELETE语句时,这是什么鬼?别担心,我们来看看:

选择数据

PDO select image

mysql_*中你所做的是什么呢?
<?php
$result = mysql_query('SELECT * from table') or die(mysql_error());

$num_rows = mysql_num_rows($result);

while($row = mysql_fetch_assoc($result)) {
    echo $row['field1'];
}

现在在PDO中,你可以这样做:
<?php
$stmt = $db->query('SELECT * FROM table');

while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
    echo $row['field1'];
}

或者

<?php
$stmt = $db->query('SELECT * FROM table');
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);

//Use $results

注意:如果您使用以下方法(query())之类的方法,该方法将返回一个PDOStatement对象。因此,如果您想获取结果,请像上面那样使用它。
<?php
foreach($db->query('SELECT * FROM table') as $row) {
    echo $row['field1'];
}

在PDO数据中,它是通过语句句柄的fetch()方法来获取的。在调用fetch之前,最好的方法是告诉PDO你希望如何获取数据。在下面的部分中,我将解释这个。
获取模式
请注意上面fetch()和fetchAll()代码中使用了PDO::FETCH_ASSOC。这告诉PDO将行作为关联数组返回,字段名作为键。还有许多其他的获取模式,我将逐一解释。
首先,我将解释如何选择获取模式:
 $stmt->fetch(PDO::FETCH_ASSOC)

在上面的代码中,我一直在使用fetch()。你也可以使用以下方法: 现在我来讲一下获取模式:
  • PDO::FETCH_ASSOC:返回一个由列名索引的数组,与结果集中返回的列名相对应
  • PDO::FETCH_BOTH(默认):返回一个由列名和以0为索引的列号索引的数组,与结果集中返回的列名和列号相对应

还有更多的选择!在PDOStatement Fetch documentation中了解所有的选择。

获取行数

不需要使用mysql_num_rows来获取返回行数,你可以获取一个PDOStatement并使用rowCount(),像这样:

<?php
$stmt = $db->query('SELECT * FROM table');
$row_count = $stmt->rowCount();
echo $row_count.' rows selected';

获取最后插入的ID
<?php
$result = $db->exec("INSERT INTO table(firstname, lastname) VAULES('John', 'Doe')");
$insertId = $db->lastInsertId();

插入、更新或删除语句

Insert and update PDO image

我们在mysql_*函数中所做的是:
<?php
$results = mysql_query("UPDATE table SET field='value'") or die(mysql_error());
echo mysql_affected_rows($result);

在PDO中,可以通过以下方式实现相同的功能:
<?php
$affected_rows = $db->exec("UPDATE table SET field='value'");
echo $affected_rows;

在上述查询中,PDO::exec执行一个SQL语句并返回受影响的行数。 插入和删除将在后面介绍。 上述方法仅在您不在查询中使用变量时有用。但是,当您需要在查询中使用变量时,千万不要像上面那样尝试,而应使用预处理语句或参数化语句

预编译语句

问:什么是预编译语句,为什么我需要它们?
答:预编译语句是一种预先编译的SQL语句,可以通过仅向服务器发送数据来多次执行。

使用预编译语句的典型工作流程如下(引用自维基百科的三个要点):

  1. 准备:应用程序创建语句模板并将其发送到数据库管理系统(DBMS)。某些值未指定,称为参数、占位符或绑定变量(下方标记为?):
`INSERT INTO PRODUCT (name, price) VALUES (?, ?)`

2. 数据库管理系统(DBMS)解析、编译并对语句模板进行查询优化,将结果存储起来而不执行它。
3. 执行:在以后的某个时间,应用程序提供(或绑定)参数的值,然后DBMS执行该语句(可能返回结果)。应用程序可以根据需要多次执行该语句,使用不同的值。在这个例子中,它可以为第一个参数提供'Bread',为第二个参数提供1.00
你可以在SQL中使用预编译语句,其中包含占位符。基本上有三种类型的占位符:没有占位符的(不要尝试使用变量,它在上面),没有命名的占位符和有命名的占位符。 问:那么,什么是命名占位符,我如何使用它们?
答:命名占位符。使用冒号前缀的描述性名称,而不是问号。我们不关心名称占位符中值的位置/顺序。
 $stmt->bindParam(':bla', $bla);

bindParam(parameter,variable,data_type,length,driver_options)

你也可以使用执行数组进行绑定:

<?php
$stmt = $db->prepare("SELECT * FROM table WHERE id=:id AND name=:name");
$stmt->execute(array(':name' => $name, ':id' => $id));
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

另一个对于面向对象编程(OOP)友好的不错特性是,命名占位符可以直接将对象插入到数据库中,前提是对象的属性与命名字段匹配。例如:
class person {
    public $name;
    public $add;
    function __construct($a,$b) {
        $this->name = $a;
        $this->add = $b;
    }

}
$demo = new person('john','29 bla district');
$stmt = $db->prepare("INSERT INTO table (name, add) value (:name, :add)");
$stmt->execute((array)$demo);
问:那么,未命名的占位符是什么,我该如何使用它们?
答:我们来看一个例子:
<?php
$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->bindValue(1, $name, PDO::PARAM_STR);
$stmt->bindValue(2, $add, PDO::PARAM_STR);
$stmt->execute();

并且

$stmt = $db->prepare("INSERT INTO folks (name, add) values (?, ?)");
$stmt->execute(array('john', '29 bla district'));

在上面,你可以看到那些?而不是像一个名称占位符一样的名称。现在在第一个例子中,我们将变量分配给各个占位符($stmt->bindValue(1, $name, PDO::PARAM_STR);)。然后,我们给这些占位符赋值并执行语句。在第二个例子中,第一个数组元素放在第一个?中,第二个数组元素放在第二个?中。
注意:在未命名的占位符中,我们必须注意将数组元素按照正确的顺序传递给PDOStatement::execute()方法。

SELECTINSERTUPDATEDELETE 预编译查询

  1. SELECT:

    $stmt = $db->prepare("SELECT * FROM table WHERE id=:id AND name=:name"); $stmt->execute(array(':name' => $name, ':id' => $id)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);

  2. INSERT:

    $stmt = $db->prepare("INSERT INTO table(field1,field2) VALUES(:field1,:field2)"); $stmt->execute(array(':field1' => $field1, ':field2' => $field2)); $affected_rows = $stmt->rowCount();

  3. DELETE:

    $stmt = $db->prepare("DELETE FROM table WHERE id=:id"); $stmt->bindValue(':id', $id, PDO::PARAM_STR); $stmt->execute(); $affected_rows = $stmt->rowCount();

  4. UPDATE:

    $stmt = $db->prepare("UPDATE table SET name=? WHERE id=?"); $stmt->execute(array($name, $id)); $affected_rows = $stmt->rowCount();


注意:

然而,PDO和/或MySQLi并不完全安全。请查看PDO准备语句是否足以防止SQL注入?的答案,作者是ircmaxell。此外,我引用了他答案中的一部分:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->query('SET NAMES GBK');
$stmt = $pdo->prepare("SELECT * FROM test WHERE name = ? LIMIT 1");
$stmt->execute(array(chr(0xbf) . chr(0x27) . " OR 1=1 /*"));

317

首先,让我们从我们给每个人的标准评论开始:

请不要在新代码中使用 mysql_* 函数。它们已不再得到维护并已正式弃用。看到红框了吗?请了解预处理语句,并使用PDOMySQLi - 本文将帮助您决定哪种方法更好。如果您选择PDO,则这里有一个很好的教程

让我们逐句解释一下:

  • 它们不再得到维护,已经被官方弃用

    这意味着PHP社区正在逐渐停止支持这些非常老的函数。它们很可能在未来(近期)的PHP版本中不存在!继续使用这些函数可能会在不远的将来破坏您的代码。

    新! - ext/mysql现在已经官方弃用自PHP 5.5开始!

    更新!ext/mysql 已在PHP 7中移除

  • 相反,您应该学习使用预处理语句

    mysql_*扩展不支持预处理语句,而这是(其他功能之一)针对SQL注入非常有效的对策。它修复了MySQL依赖应用程序中的一个非常严重的漏洞,允许攻击者访问您的脚本并在您的数据库上执行任何可能的查询

    有关更多信息,请参见如何在PHP中防止SQL注入?

  • 看到红框了吗?

    当您转到任何mysql函数手册页面时,您会看到一个红框,解释它不应再使用。

  • 使用PDO或MySQLi

    有更好、更健壮和精心构建的替代方案,PDO - PHP数据库对象,它提供了完整的面向对象的数据库交互方式,以及MySQLi,它是针对MySQL的特定改进。


4
@Mario -- PHP 开发者已经有了一个流程,并且他们刚刚投票赞成正式废弃 ext/mysql,该决定将在 5.5 版本中实施。这不再是一个假设性的问题。 - SDC
2
通过使用已经证明有效的技术,如PDO或MySQLi,添加几行额外的代码仍然能够提供PHP一直以来所提供的易用性。我希望出于开发者的利益,他/她知道在任何教程中看到这些可怕的mysql_*函数实际上会削弱课程的效果,并且应该告诉OP这种代码已经过时了——还应该质疑教程的相关性! - FredTheWebGuy
1
答案应该提到的是:预处理语句消除了IN (...)结构的任何有意义的用法。 - Eugen Rieck
@Madara的幽灵 我想知道为什么他们不用现代、更安全的代码重写mysql_*。 - Asad kamran
@asadkamran 这基本上就是 mysqli_* 的内容,但它仍然很糟糕,因为该实现的结构不好,不应使用。请改用PDO。 - Shadur
显示剩余2条评论

230

使用简便

分析和综合原因已经提到。对于新手来说,停止使用过时的mysql_函数有更重要的动力。

现代数据库API只是更加易于使用

这主要是由于可以简化代码的绑定参数。并且通过优秀的教程(如上所示),转换为PDO并不过于艰难。

然而,一次性重写较大的代码库需要时间。这就是中间替代方案的存在理由:

mysql_*等效的pdo_*函数

使用<pdo_mysql.php>,您可以轻松切换到旧的mysql_函数。它添加了pdo_函数包装器,用于替换其mysql_对应项。

  1. Simply include_once("pdo_mysql.php"); in each invocation script that has to interact with the database.

  2. Remove the mysql_ function prefix everywhere and replace it with pdo_.

    • mysql_connect() becomes pdo_connect()
    • mysql_query() becomes pdo_query()
    • mysql_num_rows() becomes pdo_num_rows()
    • mysql_insert_id() becomes pdo_insert_id()
    • mysql_fetch_array() becomes pdo_fetch_array()
    • mysql_fetch_assoc() becomes pdo_fetch_assoc()
    • mysql_real_escape_string() becomes pdo_real_escape_string()
    • and so on...

  3. Your code will work alike and still mostly look the same:

    include_once("pdo_mysql.php"); 
    
    pdo_connect("localhost", "usrABC", "pw1234567");
    pdo_select_db("test");
    
    $result = pdo_query("SELECT title, html FROM pages");  
    
    while ($row = pdo_fetch_assoc($result)) {
        print "$row[title] - $row[html]";
    }
    

现在,您的代码正在使用PDO。
现在是时候实际利用它了。

绑定参数可以很容易地使用

你只需要一个不那么笨重的API。 pdo_query()非常容易支持绑定参数。将旧代码转换很简单:

将变量从SQL字符串中移出。
  • 将它们作为逗号分隔的函数参数添加到pdo_query()中。
  • 在变量所在的位置放置问号?作为占位符。
  • 去掉之前用于包含字符串值/变量的单引号'

对于更长的代码,优势变得更加明显。

通常字符串变量不仅仅是插入到SQL中,而是与转义调用连接起来。

pdo_query("SELECT id, links, html, title, user, date FROM articles
   WHERE title='" . pdo_real_escape_string($title) . "' OR id='".
   pdo_real_escape_string($title) . "' AND user <> '" .
   pdo_real_escape_string($root) . "' ORDER BY date")

使用应用了?占位符后,您就不必再费心了:

pdo_query("SELECT id, links, html, title, user, date FROM articles
   WHERE title=? OR id=? AND user<>? ORDER BY date", $title, $id, $root)

请记住,pdo_* 仍然允许使用 任意一个
只是不要在同一查询中转义变量 并且 绑定它。
  • 占位符功能由其背后的真正 PDO 提供。
  • 因此也允许稍后使用 :named 占位符列表。

更重要的是,您可以安全地在任何查询后面传递 $_REQUEST[] 变量。当提交的 <form> 字段与数据库结构完全匹配时,甚至更短:

pdo_query("INSERT INTO pages VALUES (?,?,?,?,?)", $_POST);

非常简单。但让我们回到一些关于重写建议和技术原因的内容,解释为什么您可能希望摆脱 mysql_ 和转义。

修复或删除任何老式的sanitize() 函数

一旦您将所有 mysql_ 调用转换为带有绑定参数的 pdo_query,请删除所有冗余的 pdo_real_escape_string 调用。

特别是您应该修复任何 sanitizecleanfilterThisclean_data 函数,正如一些过时教程中所宣传的那样:

function sanitize($str) {
   return trim(strip_tags(htmlentities(pdo_real_escape_string($str))));
}

这里最明显的漏洞是缺乏文档。更重要的是,过滤的顺序完全相反了。
正确的顺序应该是:deprecatedly stripslashes 作为最内层的调用,然后是 trim,之后是 strip_tagshtmlentities 用于输出上下文,最后才是 _escape_string,因为它的应用程序应直接在 SQL 中间插入。
但是作为第一步,只需摆脱 _real_escape_string 的调用。
如果您的数据库和应用程序流程需要 HTML 上下文安全字符串,则现在可能需要保留其余的 sanitize() 函数。添加一个注释,表示此后仅适用于 HTML 转义。
字符串/值处理被委托给 PDO 及其参数化语句。
如果您的 sanitize 函数中提到了 stripslashes(),则可能表明存在更高级别的 oversight。
这通常是为了撤消来自已弃用 magic_quotes 的损坏(双重转义)。但是,这最好集中修复,而不是逐个字符串地修复。
使用其中一种 userland reversal 方法。然后删除 sanitize 函数中的 stripslashes()
有关 magic_quotes 的历史说明。该功能被正确地弃用。然而,它经常被错误地描述为失败的安全功能。但是,magic_quotes 与网球球没有作为营养来源失败一样。那不是它们的目的。
PHP2/FI 中的原始实现明确引入了它,只是“引号将自动转义,使直接将表单数据传递给 msql 查询更容易”。值得注意的是,对于 mSQL,它是安全的,因为它仅支持 ASCII。
然后 PHP3/Zend 为 MySQL 重新引入了 magic_quotes,并错误地记录了它。但最初它只是一个 方便功能,而不是用于安全。

准备语句的不同之处

当您将字符串变量混合到SQL查询中时,不仅对您而言更加复杂,MySQL再次分离代码和数据也是多余的工作。

SQL注入是指数据渗透到代码上下文中的情况。数据库服务器无法确定PHP最初将变量粘贴在查询子句之间的位置。
使用绑定参数可以在PHP代码中分离SQL代码和SQL上下文值。但它不会在幕后再次混淆(除非使用PDO::EMULATE_PREPARES)。您的数据库接收未经修改的SQL命令和1:1变量值。

虽然这个答案强调您应该关注删除 mysql_ 的可读性优势,但由于这种可见和技术数据/代码分离,有时也会出现性能优势(仅具有不同值的重复插入)。
请注意,参数绑定仍然不是针对所有 SQL 注入的魔法一站式解决方案。它处理了数据/值的最常见用途。但不能列出白名单列名/表标识符,帮助动态子句构造,或仅仅是纯数组值列表。
混合使用 PDO
这些 pdo_* 包装器函数提供了一个编码友好的临时 API。(如果没有特异功能的函数签名转换,它基本上就是 MYSQLI)。它们还在大多数时候暴露真实的 PDO。
重写不必止步于使用新的 pdo_ 函数名称。您可以逐个将每个 pdo_query() 转换为普通的 $pdo->prepare()->execute() 调用。
然而,重新简化最好从简化开始。例如,常见的结果获取:
$result = pdo_query("SELECT * FROM tbl");
while ($row = pdo_fetch_assoc($result)) {

可以用 foreach 循环代替:

foreach ($result as $row) {

或者更好的是直接和完整地检索数组:

$result->fetchAll();

在大多数情况下,与PDO或mysql_在查询失败后提供的警告相比,您将获得更多有用的警告。

其他选项

因此,这些实际原因和值得采取的途径希望可以说明放弃mysql_的实际意义。

仅仅切换到还不够。 pdo_query()也只是其前端。

除非您还引入参数绑定或可以利用较好的API中的其他内容,否则这是一个无意义的转换。我希望它足够简单,不会进一步使新手受挫。(教育通常比禁令更有效。)

虽然它符合可能起作用的最简单事物类别,但它仍然是非常实验性的代码。我只是在周末写了它。然而,有很多替代方案。只需搜索PHP数据库抽象并浏览一下即可找到许多优秀的库来处理此类任务。

如果你想进一步简化数据库交互,像Paris/Idiorm这样的映射器是值得一试的。就像没有人再使用单调的JavaScript DOM一样,现在你也不必再照看原始的数据库接口了。

9
小心使用pdo_query("INSERT INTO pages VALUES (?,?,?,?,?)", $_POST);函数 - 例如: `pdo_query("INSERT INTO users VALUES (?, ?, ?), $_POST); $_POST = array('username' => 'lawl', 'password' => '123', 'is_admin' => 'true');` - rickyduck
@Tom 当然可以,虽然它没有得到很好的维护(最后一次更新是0.9.2),但你可以创建一个fossil账户,添加到wiki或者提交错误报告(如果我没记错的话,不需要注册)。 - mario
pdo_real_escape_string() <- 这是一个真正的函数吗?我找不到任何关于它的文档。请提供一个来源。 - Ryan Stone

160

mysql_函数:

  1. 已经过时 - 它们不再得到维护
  2. 不允许您轻松地转移到另一个数据库后端
  3. 不支持预处理语句, 因此
  4. 鼓励程序员使用串联来构建查询, 导致 SQL 注入漏洞

22
#2同样适用于mysqli_ - eggyal
18
公平地说,考虑到 SQL 方言的差异,即使使用 PDO,也不能确保完全实现 #2。你需要一个适当的 ORM 包装器来实现。 - SDC
mysql_* 函数是新版本 PHP 中 mysqlnd 函数的外壳。因此,即使旧的客户端库不再维护,mysqlnd 仍然得到维护 :) - hakre
问题在于,由于过时的PHP版本,不多的Web托管提供商能够支持这种面向对象的设计风格。 - Jeff Bootsholz
@RajuGujarati 所以找一个能够做到的网络主机。如果你的网络主机不能做到,很有可能他们的服务器容易受到攻击。 - Alnitak

115
从技术方面来说,涉及这些的理由极为具体和罕见,在您的生活中极有可能永远不会用到它们。 如:
  • 非阻塞异步查询
  • 返回多个结果集的存储过程
  • 加密(SSL)
  • 压缩
如果你确实需要它们-这无疑是从mysql扩展向更时尚、现代化的东西转移的技术原因。 除此之外,还存在一些非技术问题,会让你的使用经历变得更艰难:
  • 在使用现代PHP版本进一步使用这些函数时,将引发弃用级别通知。它们可以轻松关闭。
  • 在遥远的将来,它们可能会被从默认的PHP构建中删除。这也不是什么大事,因为mydsql ext将被移动到PECL,每个主机都会很高兴地编译带有它的PHP,因为他们不想失去几十年来一直工作的客户的网站。
  • 堆栈溢出社区的强烈反对。每次你提起这些诚实的功能,你就会被告知它们受到严格的禁忌。
  • 作为一个普通的PHP用户,你使用这些函数的想法很可能是错误和容易出错的。仅因为所有这些错误的教程和手册,它们教你错误的方法。并不是函数本身-我必须强调这一点-而是它们的使用方式。
这个后一个问题是一个问题。但是,在我看来,提出的解决方案也不好。 对我而言,所有这些PHP用户立即学会如何正确处理SQL查询显得过于理想化。他们很可能只是机械地将mysql_*更改为mysqli_*,保持原有的做法不变。 尤其是因为mysqli使准备语句的使用非常痛苦和麻烦。更不用说本地的准备语句无法完全保护免受SQL注入攻击,mysqli和PDO都没有提供解决方案。 因此,与其对抗这个诚实的扩展,我更愿意与错误的做法进行斗争,并教育人们正确的方法。同时,还存在一些虚假或不重要的原因,例如:
  • 不支持存储过程(我们使用mysql_query("CALL my_proc");已经很久了)
  • 不支持事务(与上述相同)
  • 不支持多个语句(谁需要它们?)
  • 不在积极开发中(那又怎样?它是否在任何实际方面影响了你?)
  • 缺少OO接口(创建一个仅需几小时时间)
  • 不支持准备好的语句或参数化查询
最后一个是一个有趣的问题。虽然mysql ext不支持本地准备语句,但它们对于安全性并非必需品。我们可以使用手动处理的占位符轻松模拟准备语句(就像PDO一样):
function paraQuery()
{
    $args  = func_get_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);
    $result = mysql_query($query);
    if (!$result)
    {
        throw new Exception(mysql_error()." [$query]");
    }
    return $result;
}

$query  = "SELECT * FROM table where a=%s AND b LIKE %s LIMIT %d";
$result = paraQuery($query, $a, "%$b%", $limit);

voila, 一切都参数化和安全。

但是,如果您不喜欢手册中的红框,那么就会面临选择问题:mysqli 还是 PDO?

嗯,答案如下:

  • 如果您理解使用数据库抽象层的必要性并在寻找一个 API 来创建它,则 mysqli 是一个非常好的选择,因为它确实支持许多 MySQL 特定功能。
  • 如果像绝大多数 PHP 开发者一样,在应用程序代码中直接使用原始 API 调用(这本质上是错误的做法),则 PDO 是唯一的选择,因为这个扩展程序不仅是 API,而是半 DAL,虽然还不完整,但提供了许多重要特性,其中两个使 PDO 与 mysqli 显著不同:

    • 与 mysqli 不同,PDO 可以按值绑定占位符,这使得动态构建查询成为可能,而无需编写几屏相当混乱的代码。
    • 与 mysqli 不同,PDO 始终可以返回一个简单的通常数组作为查询结果,而 mysqli 只能在 mysqlnd 安装上执行此操作。

所以,如果您是一名普通的 PHP 用户,并且希望在使用原生预处理语句时节省大量麻烦,PDO 再次是唯一的选择。
但是,PDO 也不是万能药,并且有其困难之处。
因此,我在 PDO 标签维基 中编写了所有常见陷阱和复杂案例的解决方案。

尽管如此,每个谈论扩展程序的人总是忽略了两个关于 Mysqli 和 PDO 的重要事实

  1. 准备语句并不是万能的。有无法使用准备语句绑定的动态标识符。有具有未知参数数量的动态查询,这使得查询构建成为一个困难的任务。

  2. mysqli_* 和 PDO 函数都不应该出现在应用程序代码中。
    它们之间应该有一个抽象层,该抽象层将在内部执行所有脏活累活的绑定、循环、错误处理等操作,使应用程序代码变得简洁易读 (DRY and clean)。特别是对于像动态查询构建这样的复杂情况。

因此,仅仅切换到 PDO 或 mysqli 是不够的。必须使用 ORM、查询构建器或任何数据库抽象类,而不是在代码中调用原始 API 函数。
相反,如果您的应用程序代码与 MySQL API 之间有一个抽象层,则使用哪个引擎实际上并不重要。您可以使用 mysql 扩展直到它被弃用,然后轻松地将您的抽象类重写为另一个引擎,同时保留所有应用程序代码。

这里有一些基于我的safemysql 类的示例,以展示这样的抽象类应该是什么样子的:

$city_ids = array(1,2,3);
$cities   = $db->getCol("SELECT name FROM cities WHERE is IN(?a)", $city_ids);

将这一行与PDO需编写的代码进行比较。
然后再与使用原始Mysqli准备语句需要编写的大量代码进行比较。 请注意,错误处理、分析和查询日志记录已内置并正在运行。

$insert = array('name' => 'John', 'surname' => "O'Hara");
$db->query("INSERT INTO users SET ?u", $insert);

与通常的PDO插入相比,每个字段名都会重复六到十次 - 在所有这些命名占位符、绑定和查询定义中。

另一个例子:

$data = $db->getAll("SELECT * FROM goods ORDER BY ?n", $_GET['order']);

你很难找到一个PDO处理这种实际情况的例子。
而且这样会太冗长,很可能不安全。

所以,再一次强调 - 不仅应该关注原始驱动程序,还需要关注抽象类,它不仅对初学者手册中的愚蠢示例有用,还可以解决任何现实生活中遇到的问题。


21
mysql_*函数使漏洞变得非常容易出现。由于PHP被许多新手用户使用,实际上在实践中使用mysql_*甚至是有害的,即使从理论上讲,也可能没有问题。 - Madara's Ghost
4
“everything is parameterized and safe”翻译为中文是“一切都已参数化且安全”,虽然可能已经参数化,但您的函数并未使用真正的预处理语句。 - uınbɐɥs
6
“不再积极开发”只适用于虚构的“0.01%”吗?如果你使用这种静态功能构建了某些东西,一年后更新你的mysql版本并最终得到一个无法工作的系统,我相信有很多人会突然成为这个“0.01%”。我认为“废弃”和“不再积极开发”密切相关。你可以说这没有“值得的”理由,但事实是,在这两个选项之间做出选择时,“没有积极开发”几乎和“废弃”一样糟糕,我这么说可以吗? - Nanne
4
@ShaquinTrifonoff说,确实,它没有使用预处理语句。但是PDO也没有使用预处理语句,而大多数人都建议使用PDO而不是MySQLi。因此,我不确定这在此处是否有重大影响。上述代码(稍微解析一下)是PDO默认情况下准备语句时所做的事情... - ircmaxell
1
@MadaraUchiha:尽管如此,所有形式都存在同样的问题。您不认为一旦ext/mysql消失,有人会为ext/mysqli或PDO做同样的事情吗?责怪ext/mysql是愚蠢的。而且废弃它对那种风格的代码或教程没有任何影响... - ircmaxell
显示剩余2条评论

105

有很多原因,但可能最重要的原因是这些函数鼓励不安全的编程实践,因为它们不支持准备好的语句。准备好的语句有助于防止SQL注入攻击。

使用mysql_*函数时,您必须记住通过mysql_real_escape_string()运行用户提供的参数。如果您只忘记在一个地方进行转义,或者偶然只对输入的一部分进行转义,那么您的数据库可能会受到攻击。

PDOmysqli中使用准备好的语句将使这些编程错误更难发生。


3
MySQLi_*对于传递可变数量的参数(例如,当您想要传递要检查的值列表以用于IN子句时)的支持不足,这鼓励了不使用参数的做法,从而鼓励使用完全相同的连接查询,从而使MySQL_*调用易受攻击。 - Kickstart
5
再次强调,不安全并不是mysql_*函数固有的问题,而是由于错误使用导致的问题。 - Agamemnus
2
@Agamemnus 问题在于mysql_*函数使得实现“不正确的用法”变得容易,尤其是对于经验不足的程序员而言。而实现预处理语句的库则更难出现这种类型的错误。 - Trott

79

因为(除了其他原因之外),确保输入数据经过净化变得更加困难。如果使用PDO或mysqli等参数化查询,可以完全避免这种风险。

举个例子,有人可能会将"enhzflep); drop table users"作为用户名。旧的函数允许在一个查询中执行多个语句,所以像那个令人讨厌的家伙一样的东西可以删除整个表。

如果使用PDO或mysqli,用户名最终将成为"enzhflep); drop table users"

请参见bobby-tables.com


11
旧的函数将允许在每个查询中执行多个语句 - 不,它们不会。使用ext/mysql不可能进行这种类型的注入攻击 - PHP和MySQL中唯一可能发生此类注入攻击的方法是使用MySQLi和mysqli_multi_query()函数。可以通过ext/mysql和未转义的字符串进行的注入攻击是像' OR '1' = '1 这样的语句,用于提取不应被访问的数据库数据。在某些情况下,可以注入子查询,但仍然无法以此方式修改数据库。 - DaveRandom
是的,而且要进行多个查询真是让人头疼。Mysqli*也是一样的,至少在PHP 5.2之前是这样的(它不是自动的,你确实需要使用mysqli_multi_query [https://www.php.net/manual/en/mysqli.multi-query.php][source])。 - undefined

75

本答案旨在展示如何绕过编写不良的PHP用户验证代码,这些攻击使用什么方法,并如何使用安全的预处理语句替换旧的MySQL函数-以及基本上,为什么StackOverflow用户(可能拥有很多声望)会对要求改进他们的代码的新用户进行攻击。

首先,请随意创建此测试MySQL数据库(我已将其称为prep):

mysql> create table users(
    -> id int(2) primary key auto_increment,
    -> userid tinytext,
    -> pass tinytext);
Query OK, 0 rows affected (0.05 sec)

mysql> insert into users values(null, 'Fluffeh', 'mypass');
Query OK, 1 row affected (0.04 sec)

mysql> create user 'prepared'@'localhost' identified by 'example';
Query OK, 0 rows affected (0.01 sec)

mysql> grant all privileges on prep.* to 'prepared'@'localhost' with grant option;
Query OK, 0 rows affected (0.00 sec)

完成了这一步,我们可以转向我们的PHP代码。

假设下面的脚本是一个网站管理员验证过程的验证程序(如果您复制并使用它进行测试,则是简化但可工作的):

<?php 

    if(!empty($_POST['user']))
    {
        $user=$_POST['user'];
    }   
    else
    {
        $user='bob';
    }
    if(!empty($_POST['pass']))
    {
        $pass=$_POST['pass'];
    }
    else
    {
        $pass='bob';
    }
    
    $database='prep';
    $link=mysql_connect('localhost', 'prepared', 'example');
    mysql_select_db($database) or die( "Unable to select database");

    $sql="select id, userid, pass from users where userid='$user' and pass='$pass'";
    //echo $sql."<br><br>";
    $result=mysql_query($sql);
    $isAdmin=false;
    while ($row = mysql_fetch_assoc($result)) {
        echo "My id is ".$row['id']." and my username is ".$row['userid']." and lastly, my password is ".$row['pass']."<br>";
        $isAdmin=true;
        // We have correctly matched the Username and Password
        // Lets give this person full access
    }
    if($isAdmin)
    {
        echo "The check passed. We have a verified admin!<br>";
    }
    else
    {
        echo "You could not be verified. Please try again...<br>";
    }
    mysql_close($link);

?>

<form name="exploited" method='post'>
    User: <input type='text' name='user'><br>
    Pass: <input type='text' name='pass'><br>
    <input type='submit'>
</form>

一开始看起来似乎很合理。

用户必须输入登录名和密码,对吗?

太棒了!现在请输入以下内容:

user: bob
pass: somePass

并提交它。

输出如下:

You could not be verified. Please try again...

太好了!运行正常,现在让我们尝试实际的用户名和密码:

user: Fluffeh
pass: mypass

太棒了!高五围成一圈,代码正确地验证了管理员身份。它是完美的!

不过,实际上并不是这样。假设用户很聪明,比如说这个人就是我。请输入以下内容:

user: bob
pass: n' or 1=1 or 'm=m

输出结果为:

The check passed. We have a verified admin!
恭喜您,通过输入虚假的用户名和密码,您刚刚让我进入了您超级保护的管理员专属区。如果您不相信我,请使用我提供的代码创建数据库并运行这段PHP代码 - 一眼看上去似乎确实很好地验证了用户名和密码。 因此,回答您的问题,这就是为什么人们对您大声呼喊的原因。 让我们来看看出了什么问题,以及为什么我刚刚进入了您的超级管理员专属蝙蝠洞。我猜测您不会小心处理输入,只是将它们直接传递给数据库。我以一种可以更改您实际运行的查询的方式构造了输入。那么,它应该是什么,最终变成了什么?
select id, userid, pass from users where userid='$user' and pass='$pass'

这是查询语句,但当我们用实际的输入替换变量时,得到以下结果:

select id, userid, pass from users where userid='bob' and pass='n' or 1=1 or 'm=m'

看看我如何构造我的“密码”,以便它首先关闭围绕密码的单引号,然后引入一个全新的比较?为了安全起见,我添加了另一个“字符串”,以便单引号在我们最初编写的代码中得到预期的关闭。

然而,这不是关于人们现在对你大喊大叫的事情,而是展示如何使您的代码更加安全。

好的,那么出了什么问题,我们该如何修复呢?

这是一种经典的SQL注入攻击。其中最简单的之一。在攻击向量的规模上,这就像是一个蹒跚学步的孩子攻击一辆坦克 - 并且获胜了。

那么,如何保护您神圣的管理部分并使其更加安全呢?首先要做的是停止使用那些非常古老和已弃用的mysql_*函数。我知道,你遵循了一份在网上找到的教程,并且它确实有效,但它已经过时,几分钟内,我就轻松地突破了它。

现在,您有更好的选择,可以使用mysqli_PDO。我个人非常喜欢PDO,因此在本答案的其余部分中将使用PDO。它有优点和缺点,但个人认为优点远远超过缺点。它可在多个数据库引擎之间移植 - 无论您是使用MySQL还是Oracle或任何其他东西 - 只需更改连接字符串即可,它具有我们想要使用的所有高级功能,并且代码清晰易懂。我喜欢简洁。

现在,让我们再次查看那段代码,这次使用PDO对象编写:

<?php 

    if(!empty($_POST['user']))
    {
        $user=$_POST['user'];
    }   
    else
    {
        $user='bob';
    }
    if(!empty($_POST['pass']))
    {
        $pass=$_POST['pass'];
    }
    else
    {
        $pass='bob';
    }
    $isAdmin=false;
    
    $database='prep';
    $pdo=new PDO ('mysql:host=localhost;dbname=prep', 'prepared', 'example');
    $sql="select id, userid, pass from users where userid=:user and pass=:password";
    $myPDO = $pdo->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
    if($myPDO->execute(array(':user' => $user, ':password' => $pass)))
    {
        while($row=$myPDO->fetch(PDO::FETCH_ASSOC))
        {
            echo "My id is ".$row['id']." and my username is ".$row['userid']." and lastly, my password is ".$row['pass']."<br>";
            $isAdmin=true;
            // We have correctly matched the Username and Password
            // Lets give this person full access
        }
    }
    
    if($isAdmin)
    {
        echo "The check passed. We have a verified admin!<br>";
    }
    else
    {
        echo "You could not be verified. Please try again...<br>";
    }

?>

<form name="exploited" method='post'>
    User: <input type='text' name='user'><br>
    Pass: <input type='text' name='pass'><br>
    <input type='submit'>
</form>
主要区别在于不再使用mysql_*函数,现在所有操作都通过PDO对象完成,并且使用预处理语句。什么是预处理语句?它是一种在运行查询之前告诉数据库要运行的查询的方法。在这种情况下,我们告诉数据库:“嗨,我将运行一个选择语句,想要从表user中选择id、userid和pass,其中userid是一个变量,pass也是一个变量。” 然后,在执行语句中,我们传递了一个包含所有变量的数组给数据库,使其能够按预期工作。结果非常棒。让我们再次尝试之前的那些用户名和密码组合:
user: bob
pass: somePass

用户未经过验证。太棒了。

如何:

user: Fluffeh
pass: mypass

哦,我有点兴奋了,它起作用了:检查通过。我们有一个经过验证的管理员!

现在,让我们尝试一下一个聪明的家伙可能输入以尝试绕过我们的验证系统的数据:

user: bob
pass: n' or 1=1 or 'm=m

这次,我们得到以下结果:

You could not be verified. Please try again...

你被喊叫的原因是发帖提问时,人们能够看到你的代码可以轻易地被绕过。请使用这个问答来改进你的代码,使其更加安全,并使用当前的函数。

最后,这并不是说这是完美的代码。你可以做很多事情来改进它,例如使用散列密码,确保在数据库中存储敏感信息时,不要以明文形式存储它,有多个验证级别等等。但如果你仅将旧的注入易受攻击的代码更改为此代码,你就已经朝着编写良好代码的方向迈出了很大的一步。而且你已经到了这个地步并且仍在阅读,这给我带来了希望,希望你不仅在编写网站和应用程序时实施这种类型的代码,而且还能去研究我刚才提到的其他事情——以及更多。尽你所能编写最好的代码,而不是只能勉强运行的基本代码。


4
谢谢您的回答!我给您点赞!值得注意的是,mysql_* 本身并不是不安全的,但是由于糟糕的教程和缺乏适当的语句准备 API,它会促使编写不安全的代码。 - Madara's Ghost

39

MySQL扩展是三种扩展中最古老的,也是开发人员最初用来与MySQL通信的原始方式。由于PHP和MySQL的新版本中所做的改进,这个扩展现在已经被弃用,而推荐使用其他两个 替代方案

  • MySQLi 是用于操作MySQL数据库的“改良版”扩展。它利用了在新版本MySQL服务器中可用的功能,向开发者提供面向函数和面向对象的接口,并进行了一些其他巧妙的处理。

  • PDO 提供了一个API,将以前分散在主要数据库访问扩展(如MySQL、PostgreSQL、SQLite、MSSQL等)上的大部分功能进行了整合。该接口向程序员暴露了高级对象,用于处理数据库连接、查询和结果集,而低级驱动程序与数据库服务器完成通信和资源处理。PDO受到了广泛的讨论和工作,被认为是在现代、专业代码中处理数据库的适当方法。


是的,但我不得不创建一个可以与Oracle和MySQL一起使用的数据库驱动程序,我发现PDO并不是Oracle官方支持的,所以有这个问题:( @Alexander对PDO的解释很棒。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接