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

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

为什么我应该使用其他东西,即使他们在我的网站上工作?

如果他们不工作在我的网站上,为什么我得到这样的错误

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

251361 次浏览

mysql_函数:

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

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

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

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

MySQL扩展:

  • 未在积极开发中
  • 从PHP 5.5开始是正式宣布已弃用(2013年6月发布)。
  • 自PHP 7.0起排名第0(2015年12月发布)
    • 这意味着从2018年12月31日开始,它不存在于任何受支持的PHP版本中。如果您使用的是支持它的PHP版本,则您使用的是没有修复安全问题的版本。
  • 缺少OO接口
  • 不支持:
    • 非阻塞、异步查询
    • 准备好的语句或参数化查询
    • 存储过程
    • 多个语句
    • 交易
    • “新”密码身份验证方法(在MySQL 5.6中默认开启;5.7中需要)
    • MySQL 5.1或更高版本中的任何新功能

由于它已弃用,使用它会使您的代码更少的未来证明。

缺乏对预准备语句的支持尤其重要,因为它们提供了一种更清晰、更不容易出错的转义和引用外部数据的方法,而不是使用单独的函数调用手动转义它。

SQL扩展的比较

因为(除其他原因外)确保输入数据被净化要困难得多。如果您使用参数化查询,就像使用PDO或mysqli一样,您可以完全避免风险。

例如,有人可以使用"enhzflep); drop table users"作为用户名。旧函数将允许每个查询执行多个语句,因此像那个讨厌的bugger这样的东西可以删除整个表。

如果使用mysqli的PDO,用户名最终将为"enhzflep); drop table users"

bobby-tables.com

首先,让我们从我们给大家的标准评论开始:

请不要在新代码中使用#0函数。它们不再维护并被正式弃用。看到红盒子了吗?改为了解准备好的声明,并使用PDOMySQLi-这篇文章将帮助您决定哪个。如果您选择PDO,这里有一个很好的教程

让我们通过这个,逐句,并解释:

  • 它们不再被维护,并被正式弃用

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

    从PHP 5.5正式弃用!

    mysql在PHP 7中被删除了.
  • 相反,你应该学习准备好的陈述

    mysql_*扩展不支持准备好的声明,这是针对SQL注射的非常有效的对策。它修复了MySQL依赖应用程序中的一个非常严重的漏洞,该漏洞允许攻击者访问您的脚本并在您的数据库上执行任何可能的查询

    有关详细信息,请参阅如何防止PHP中的SQL注入?

  • 看到红盒子了吗?

    当您转到任何mysql函数手册页时,您会看到一个红色框,说明它不应该再使用了。

  • 使用PDO或MySQLi

    有更好、更健壮和构建良好的替代方案,PDO-PHP数据库对象为数据库交互提供了完整的OOP方法,MySQLi是MySQL特定的改进。

PHP提供了三种不同的API来连接MySQL。这些是#0(从PHP 7开始删除)、#1#2扩展。

mysql_*函数曾经非常流行,但现在不再鼓励使用它们了。留档团队正在讨论数据库安全情况,教育用户远离常用的ext-mysql扩展是其中的一部分(检查php.internals:不建议使用ext-mysql)。

后来的PHP开发团队决定在用户连接到MySQL时生成#0错误,无论是通过mysql_connect()mysql_pconnect()还是ext/mysql内置的隐式连接功能。

#0自PHP 5.5起正式弃用,一直是自PHP 7起删除

看到红盒子了吗?

当您进入任何mysql_*函数手册页时,您会看到一个红色框,说明它不应该再使用了。

为什么


远离ext/mysql不仅与安全性有关,还与访问MySQL数据库的所有功能有关。

ext/mysql是为MySQL3.23构建的,从那时起只有很少的添加,同时大部分保持与这个旧版本的兼容性,这使得代码更难维护。ext/mysql不支持的缺失功能包括:(从PHP手册)。

不使用#0函数的原因

  • 不在积极开发中
  • 从PHP 7开始删除
  • 缺少OO接口
  • 不支持非阻塞、异步查询
  • 不支持预准备语句或参数化查询
  • 不支持存储过程
  • 不支持多个语句
  • 不支持交易
  • 不支持MySQL 5.1中的所有功能

以上引自昆汀的回答

缺乏对预准备语句的支持尤其重要,因为它们提供了一种更清晰、更不容易出错的转义和引用外部数据的方法,而不是使用单独的函数调用手动转义它。

SQL扩展的比较


抑制弃用警告

当代码转换为MySQLi/PDO时,可以通过在php.ini中设置error_reporting来排除E_DEPRECATED:来抑制E_DEPRECATED错误

error_reporting = E_ALL ^ E_DEPRECATED

请注意,这也将隐藏其他弃用警告,但是,它可能适用于MySQL以外的事物。(从PHP手册

文章PDO vs. MySQLi:你应该使用哪个? byDejan Marjanovic将帮助您选择。

更好的方法是PDO,我现在正在编写一个简单的PDO教程。


一个简单而简短的PDO教程


问:我脑海中的第一个问题是:什么是“PDO”?

A."PDO-PHP数据对象-是一个数据库访问层,提供对多个数据库的统一访问方法。"

alt文本


连接到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(数据源名称)和可选的usernamepassword

在这里,我想你已经熟悉了除了DSN之外的所有内容;这是PDO中的新内容。DSN基本上是一串选项,告诉PDO要使用哪个驱动程序,以及连接细节。如需进一步参考,请查看PDO MySQL DSN

$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'username', 'password');

备注:你也可以使用charset=UTF-8,但有时会导致错误,所以最好使用utf8

如果有任何连接错误,它将抛出一个PDOException对象,该对象可以被捕获以进一步处理Exception

读得不错连接和连接管理

您还可以将多个驱动程序选项作为数组传递给第四个参数。我建议传递将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所做的任何事情都应该包装在trycatch块中。我们可以通过设置错误模式属性强制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 usersome_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 latertry {data_fun($db);}catch(PDOException $ex) {//Here you can handle error and show message/perform action you want.}

此外,您可以按or die()处理,或者我们可以说像mysql_*,但它会非常不同。您可以通过转向display_errors off并仅阅读您的错误日志来隐藏生产中的危险错误消息。

现在,在阅读了上面的所有内容之后,你可能在想:当我只想开始学习简单的SELECTINSERTUPDATEDELETE语句时,到底是什么?别担心,我们开始吧:


选择数据

PDO选择图像

所以你在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对象。因此,如果您想获取结果,请像上面一样使用它。

<?phpforeach($db->query('SELECT * FROM table') as $row) {echo $row['field1'];}

在PDO Data中,它是通过->fetch()获得的,这是您的语句句柄的方法。在调用提取之前,最好的方法是告诉PDO您希望如何获取数据。在下面的部分中,我将对此进行解释。

获取模式

请注意,在上面的fetch()fetchAll()代码中使用了PDO::FETCH_ASSOC。这告诉PDO将行作为关联数组返回,字段名作为键。还有许多其他获取模式,我将逐一解释。

首先,我解释如何选择获取模式:

 $stmt->fetch(PDO::FETCH_ASSOC)

在上面,我一直在使用fetch()。您也可以使用:

  • #0-返回一个包含所有结果集行的数组
  • #0-从结果集的下一行返回一列
  • #0-获取下一行并将其作为对象返回。
  • #0-设置此语句的默认获取模式

现在我来获取模式:

  • PDO::FETCH_ASSOC:返回结果集中返回的按列名索引的数组
  • PDO::FETCH_BOTH(默认):返回一个数组,该数组由结果集中返回的列名和0索引列号索引

还有更多的选择!在#0留档。中阅读它们。

获取行数

您可以获取PDOStatement并执行rowCount(),而不是使用mysql_num_rows来获取返回的行数,例如:

<?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();

插入和更新或删除语句

插入并更新PDO图像

我们在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;

在上面的查询#0中,执行SQL语句并返回受影响的行数。

插入和删除将在稍后介绍。

上述方法仅在您不在查询中使用变量时有用。但是当您需要在查询中使用变量时,永远不要像上面那样尝试,因为准备语句或参数化语句是。


准备的发言稿

Q.什么是准备好的语句,为什么我需要它们?
准备好的语句是预编译的SQL语句,可以通过仅向服务器发送数据来多次执行。

使用预准备语句的典型工作流程如下(引用自维基百科3个点):

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

    INSERT INTO PRODUCT (name, price) VALUES (?, ?)

  2. DBMS对语句模板进行解析、编译和执行查询优化,并存储结果而不执行它。

  3. 执行:稍后,应用程序为参数提供(或绑定)值,DBMS执行语句(可能返回结果)。应用程序可以使用不同的值多次执行语句。在这个例子中,它可能为第一个参数提供'Bread',为第二个参数提供1.00

您可以通过在SQL中包含占位符来使用准备好的语句。基本上有三个没有占位符的语句(不要尝试上面的变量),一个带有未命名占位符,一个带有命名占位符。

Q.那么现在,什么是命名占位符以及如何使用它们?
A.命名占位符。使用冒号前面的描述性名称,而不是问号。我们不关心名称占位符中值的位置/顺序:

 $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);

Q.那么现在,什么是未命名的占位符以及如何使用它们?
A.让我们举个例子:

<?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. #0

    $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();

NOTE:

However PDO and/or MySQLi are not completely safe. Check the answer Are PDO prepared statements sufficient to prevent SQL injection? by ircmaxell. Also, I am quoting some part from his answer:

$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 /*"));

说到0个原因,只有几个,非常具体,很少使用。很可能你一辈子都不会使用它们。
也许我太无知了,但我从来没有机会使用它们像

  • 非阻塞、异步查询
  • 返回多个结果集的存储过程
  • 加密(SSL)
  • 压缩

如果您需要它们-这些无疑是从mysql扩展转向更时尚和现代外观的技术原因。

不过也有一些非技术性的问题这可以使您的经验有点困难

  • 在现代PHP版本中进一步使用这些函数将引发弃用级别的通知。它们可以简单地关闭。
  • 在遥远的将来,它们可能会从默认的PHP构建中删除。这也没什么大不了的,因为mydsql ext将被移入PECL,每个主机都很乐意用它编译PHP,因为他们不想失去网站工作了几十年的客户。
  • 来自Stackoverflow社区的强烈抵制。每次你提到这些诚实的函数,你都被告知它们是严格的禁忌。
  • 作为一个普通的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);

,一切都是参数化和安全的。

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

那么,答案如下:

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

    • 与mysqli不同,PDO可以绑定占位符按价值,这使得动态构建的查询变得可行,而无需几个非常混乱的代码屏幕。
    • 与mysqli不同,PDO总是可以在一个简单的普通数组中返回查询结果,而mysqli只能在mysqlnd安装上执行。

因此,如果您是一个普通的PHP用户,并且想在使用本机预准备语句时为自己节省大量麻烦,那么PDO-再次-是唯一的选择。
然而,PDO也不是银弹,有其艰辛。所以,我为PDO标签wiki中的所有常见陷阱和复杂情况编写了解决方案

然而,每个谈论扩展的人总是错过关于Mysqli和PDO的2重要事实

  1. 准备好的语句不是灵丹妙药。有些动态标识符不能使用准备好的语句绑定。有些动态查询具有未知数量的参数,这使得查询构建成为一项困难的任务。

  2. mysqli_*和PDO函数都不应该出现在应用程序代码中。
    它们和应用程序代码之间应该有一个抽象层,它将在内部完成绑定、循环、错误处理等所有肮脏的工作,使应用程序代码干净利落。特别是对于像动态查询构建这样的复杂情况。

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

以下是一些基于我的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处理这种实际情况的例子。
这将是太冗长,最有可能不安全。

所以,再一次-它不仅仅是原始驱动程序应该是你关心的问题,而是抽象类,不仅对初学者手册中的愚蠢示例有用,而且可以解决任何现实生活中的问题。

编写这个答案是为了说明绕过编写不佳的PHP用户验证代码是多么微不足道,这些攻击如何(以及使用什么)工作,以及如何用安全的准备好的语句替换旧的MySQL函数-基本上,为什么StackOverflow用户(可能有很多代表)对新用户提出问题以改进他们的代码。

首先,请随意创建这个测试mysql数据库(我已经调用了我的准备):

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: bobpass: somePass

并提交。

输出如下:

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

超级!按预期工作,现在让我们尝试实际的用户名和密码:

user: Fluffehpass: mypass

太棒了!四面楚歌,代码正确验证了管理员。太完美了!

嗯,不完全是。假设用户是一个聪明的小人。假设这个人是我。

输入以下内容:

user: bobpass: 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对象完成的,其次,它使用的是准备好的语句。现在,你问的是什么是准备好的语句?这是一种在运行查询之前告诉数据库,我们要运行的查询是什么的方法。在这种情况下,我们告诉数据库:“嗨,我要运行一个选择语句,需要id、userid和从表用户传递,其中userid是一个变量,pass也是一个变量。”。

然后,在执行语句中,我们向数据库传递一个数组,其中包含它现在期望的所有变量。

结果非常棒。让我们再次尝试之前的用户名和密码组合:

user: bobpass: somePass

用户未经验证。太棒了。

不如:

user: Fluffehpass: mypass

哦,我只是有点兴奋,它奏效了:支票通过了。我们有一个经过验证的管理员!

现在,让我们尝试一个聪明的家伙会输入的数据,试图通过我们的小验证系统:

user: bobpass: n' or 1=1 or 'm=m

这一次,我们得到了以下内容:

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

这就是为什么你在发布问题时被大喊大叫的原因-这是因为人们可以看到你的代码甚至不尝试就可以绕过。请使用此问题和答案来改进你的代码,使其更安全并使用最新的函数。

最后,这并不是说这是完美的代码。你可以做很多事情来改进它,例如,使用散列密码,确保在数据库中存储敏感信息时,你不会以纯文本形式存储,有多个级别的验证-但实际上,如果你只是将旧的易注入代码更改为此,你将很好地编写好的代码-事实上,你已经走了这么远,还在阅读,这给了我一种希望,你不仅会在编写网站和应用程序时实现这种类型的代码,而且你可能会出去研究我刚才提到的其他事情-等等。写你能写的最好的代码,而不是几乎没有功能的最基本的代码。

易用性

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

当代数据库API只是更容易使用。

它主要是约束参数,可以简化代码。使用优秀的教程(如上所示),过渡到PDO并不困难。

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

等效pdo_*函数代替mysql_*

使用<pdo_mysql.php>,您可以使用最小的努力从旧的mysql_函数切换。它添加了pdo_函数包装器来替换它们的mysql_对应物。

  1. 在每个必须与数据库交互的调用脚本中简单地include_once(#1);

  2. 删除#0函数前缀到处都是并将其替换为#1

    • #0connect()变成了#2connect()
    • #0query()变成了#2query()
    • #0num_rows()变成了#2num_rows()
    • #0insert_id()变成了#2insert_id()
    • #0fetch_array()变成了#2fetch_array()
    • #0fetch_assoc()变成了#2fetch_assoc()
    • #0real_escape_string()变成了#2real_escape_string()
    • 等等…

  3. 您的代码将类似地工作,并且大部分看起来仍然相同:

    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]";}

Et voilà.
Your code is using PDO.
Now it's time to actually utilize it.

Bound parameters can be easy to use

You just need a less unwieldy API.

pdo_query() adds very facile support for bound parameters. Converting old code is straightforward:

Move your variables out of the SQL string.

  • Add them as comma delimited function parameters to pdo_query().
  • Place question marks ? as placeholders where the variables were before.
  • Get rid of ' single quotes that previously enclosed string values/variables.

The advantage becomes more obvious for lengthier code.

Often string variables aren't just interpolated into SQL, but concatenated with escaping calls in between.

pdo_query("SELECT id, links, html, title, user, date FROM articlesWHERE 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 articlesWHERE title=? OR id=? AND user<>? ORDER BY date", $title, $id, $root)

请记住,pdo_*仍然允许要么要么
只是不要转义变量将其绑定在同一个查询中。

  • 占位符功能由其背后的真实PDO提供。
  • 因此也允许稍后使用:named占位符列表。

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

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

这么简单。但是让我们回到一些重写建议和技术原因,为什么你可能想摆脱#0和转义。

修复或删除任何旧学校#0功能

使用绑定参数将所有#0调用转换为pdo_query后,删除所有冗余的pdo_real_escape_string调用。

特别是,您应该修复任何sanitizecleanfilterThisclean_data函数,就像过时的教程以一种或另一种形式宣传的那样:

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

最明显的bug是没有留档,更重要的是过滤的顺序完全错了。

  • 正确的顺序应该是:弃用stripslashes作为最里面的调用,然后trim,之后strip_tagshtmlentities用于输出上下文,最后_escape_string作为其应用程序应该直接在SQL交错之前。

  • 但作为第一步,只需摆脱_real_escape_string调用。

  • 如果数据库和应用程序流需要超文本标记语言-上下文安全的字符串,则可能需要暂时保留sanitize()函数的其余部分。

  • 字符串/值处理委托给PDO及其参数化语句。

  • 如果您的清理函数中提到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变量值。

虽然这个答案强调你应该关心删除#0的易读性优势。由于这种可见的和技术性的数据/代码分离,偶尔也会有性能优势(重复的INSERT只有不同的值)。

请注意,参数绑定仍然不是针对所有SQL注入的神奇一站式解决方案。它处理数据/值的最常见用途。但不能将列名/表标识符列入白名单,帮助动态子句构造,或者只是普通数组值列表。

混合PDO使用

这些pdo_*包装器函数构成了一个编码友好的权宜之计API。(如果不是因为特殊的函数签名偏移,这几乎就是MYSQLI可能的样子)。它们在大多数情况下还暴露了真正的PDO。
重写不必停止使用新的pdo_函数名称。您可以逐个将每个pdo_query()转换为普通的$pdo->准备()->执行()调用。

不过,最好从简化开始。例如常见的结果获取:

$result = pdo_query("SELECT * FROM tbl");while ($row = pdo_fetch_assoc($result)) {

可以替换为仅一个foreach迭代:

foreach ($result as $row) {

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

$result->fetchAll();

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

其他选择

因此,这有望可视化一些实际原因和一个有价值的途径来放弃#0

只是切换到并不能完全切断它。pdo_query()也只是它的前端。

除非你也引入参数绑定或者可以利用更好的API中的其他东西,否则这是一个毫无意义的切换。我希望它被描述得足够简单,不会进一步阻碍新人。(教育通常比禁止更好。)

虽然它符合最简单的可能工作的类别,但它仍然是非常实验性的代码。我周末刚刚写了它。然而,有太多的替代方案。只需谷歌搜索PHP数据库抽象并浏览一点。一直都有很多优秀的库用于此类任务。

如果你想进一步简化数据库交互,像Paris/Idiorm这样的映射器值得一试。就像没有人再使用JavaScript中平淡无奇的DOM一样,现在你不必照看原始数据库界面。

MySQL扩展是三个扩展中最古老的,也是开发人员用来与MySQL通信的原始方式。由于PHP和MySQL的新版本都做出了改进,这个扩展现在是已弃用,而不是其他两个替代品

  • MySQLi是用于处理MySQL数据库的“改进”扩展。它利用了较新版本的MySQL服务器中可用的功能,向开发人员公开了面向函数和面向对象的接口,并做了一些其他漂亮的事情。

  • PDO提供了一个API,它整合了以前分布在主要数据库访问扩展中的大部分功能,即MySQL、PostgreSQL、SQLite、MSSQL等。该接口公开了高级对象,供程序员处理数据库连接、查询和结果集,低级驱动程序执行与数据库服务器的通信和资源处理。关于PDO有很多讨论和工作,它被认为是在现代专业代码中处理数据库的适当方法。

我发现上面的答案很长,所以总结一下:

mysqli扩展有许多好处,关键增强mysql扩展是:

  • 面向对象接口
  • 支持准备好的声明
  • 支持多个语句
  • 支持交易
  • 增强的调试能力
  • 嵌入式服务器支持

来源:MySQLi概述


正如上面的答案所解释的,mysql的替代品是mysqli和PDO(PHP数据对象)。

  • API支持服务器端预准备语句:由MYSQLi和PDO支持
  • API支持客户端预准备语句:仅由PDO支持
  • API支持存储过程:MySQLi和PDO
  • API支持多个语句和所有MySQL 4.1+功能-由MySQLi支持,大部分也由PDO支持

MySQLi和PDO都是在PHP 5.0中引入的,而MySQL是在PHP 3.0之前引入的。需要注意的是,MySQL包含在PHP5. x中,尽管在以后的版本中已弃用。

可以使用mysqli或PDO定义几乎所有mysql_*函数。只需将它们包含在旧的PHP应用程序之上,它就可以在PHP7上运行。我的解决方案这里

<?php
define('MYSQL_LINK', 'dbl');$GLOBALS[MYSQL_LINK] = null;
function mysql_link($link=null) {return ($link === null) ? $GLOBALS[MYSQL_LINK] : $link;}
function mysql_connect($host, $user, $pass) {$GLOBALS[MYSQL_LINK] = mysqli_connect($host, $user, $pass);return $GLOBALS[MYSQL_LINK];}
function mysql_pconnect($host, $user, $pass) {return mysql_connect($host, $user, $pass);}
function mysql_select_db($db, $link=null) {$link = mysql_link($link);return mysqli_select_db($link, $db);}
function mysql_close($link=null) {$link = mysql_link($link);return mysqli_close($link);}
function mysql_error($link=null) {$link = mysql_link($link);return mysqli_error($link);}
function mysql_errno($link=null) {$link = mysql_link($link);return mysqli_errno($link);}
function mysql_ping($link=null) {$link = mysql_link($link);return mysqli_ping($link);}
function mysql_stat($link=null) {$link = mysql_link($link);return mysqli_stat($link);}
function mysql_affected_rows($link=null) {$link = mysql_link($link);return mysqli_affected_rows($link);}
function mysql_client_encoding($link=null) {$link = mysql_link($link);return mysqli_character_set_name($link);}
function mysql_thread_id($link=null) {$link = mysql_link($link);return mysqli_thread_id($link);}
function mysql_escape_string($string) {return mysql_real_escape_string($string);}
function mysql_real_escape_string($string, $link=null) {$link = mysql_link($link);return mysqli_real_escape_string($link, $string);}
function mysql_query($sql, $link=null) {$link = mysql_link($link);return mysqli_query($link, $sql);}
function mysql_unbuffered_query($sql, $link=null) {$link = mysql_link($link);return mysqli_query($link, $sql, MYSQLI_USE_RESULT);}
function mysql_set_charset($charset, $link=null){$link = mysql_link($link);return mysqli_set_charset($link, $charset);}
function mysql_get_host_info($link=null) {$link = mysql_link($link);return mysqli_get_host_info($link);}
function mysql_get_proto_info($link=null) {$link = mysql_link($link);return mysqli_get_proto_info($link);}function mysql_get_server_info($link=null) {$link = mysql_link($link);return mysqli_get_server_info($link);}
function mysql_info($link=null) {$link = mysql_link($link);return mysqli_info($link);}
function mysql_get_client_info() {$link = mysql_link();return mysqli_get_client_info($link);}
function mysql_create_db($db, $link=null) {$link = mysql_link($link);$db = str_replace('`', '', mysqli_real_escape_string($link, $db));return mysqli_query($link, "CREATE DATABASE `$db`");}
function mysql_drop_db($db, $link=null) {$link = mysql_link($link);$db = str_replace('`', '', mysqli_real_escape_string($link, $db));return mysqli_query($link, "DROP DATABASE `$db`");}
function mysql_list_dbs($link=null) {$link = mysql_link($link);return mysqli_query($link, "SHOW DATABASES");}
function mysql_list_fields($db, $table, $link=null) {$link = mysql_link($link);$db = str_replace('`', '', mysqli_real_escape_string($link, $db));$table = str_replace('`', '', mysqli_real_escape_string($link, $table));return mysqli_query($link, "SHOW COLUMNS FROM `$db`.`$table`");}
function mysql_list_tables($db, $link=null) {$link = mysql_link($link);$db = str_replace('`', '', mysqli_real_escape_string($link, $db));return mysqli_query($link, "SHOW TABLES FROM `$db`");}
function mysql_db_query($db, $sql, $link=null) {$link = mysql_link($link);mysqli_select_db($link, $db);return mysqli_query($link, $sql);}
function mysql_fetch_row($qlink) {return mysqli_fetch_row($qlink);}
function mysql_fetch_assoc($qlink) {return mysqli_fetch_assoc($qlink);}
function mysql_fetch_array($qlink, $result=MYSQLI_BOTH) {return mysqli_fetch_array($qlink, $result);}
function mysql_fetch_lengths($qlink) {return mysqli_fetch_lengths($qlink);}
function mysql_insert_id($qlink) {return mysqli_insert_id($qlink);}
function mysql_num_rows($qlink) {return mysqli_num_rows($qlink);}
function mysql_num_fields($qlink) {return mysqli_num_fields($qlink);}
function mysql_data_seek($qlink, $row) {return mysqli_data_seek($qlink, $row);}
function mysql_field_seek($qlink, $offset) {return mysqli_field_seek($qlink, $offset);}
function mysql_fetch_object($qlink, $class="stdClass", array $params=null) {return ($params === null)? mysqli_fetch_object($qlink, $class): mysqli_fetch_object($qlink, $class, $params);}
function mysql_db_name($qlink, $row, $field='Database') {mysqli_data_seek($qlink, $row);$db = mysqli_fetch_assoc($qlink);return $db[$field];}
function mysql_fetch_field($qlink, $offset=null) {if ($offset !== null)mysqli_field_seek($qlink, $offset);return mysqli_fetch_field($qlink);}
function mysql_result($qlink, $offset, $field=0) {if ($offset !== null)mysqli_field_seek($qlink, $offset);$row = mysqli_fetch_array($qlink);return (!is_array($row) || !isset($row[$field]))? false: $row[$field];}
function mysql_field_len($qlink, $offset) {$field = mysqli_fetch_field_direct($qlink, $offset);return is_object($field) ? $field->length : false;}
function mysql_field_name($qlink, $offset) {$field = mysqli_fetch_field_direct($qlink, $offset);if (!is_object($field))return false;return empty($field->orgname) ? $field->name : $field->orgname;}
function mysql_field_table($qlink, $offset) {$field = mysqli_fetch_field_direct($qlink, $offset);if (!is_object($field))return false;return empty($field->orgtable) ? $field->table : $field->orgtable;}
function mysql_field_type($qlink, $offset) {$field = mysqli_fetch_field_direct($qlink, $offset);return is_object($field) ? $field->type : false;}
function mysql_free_result($qlink) {try {mysqli_free_result($qlink);} catch (Exception $e) {return false;}return true;}

不要使用mysql,因为不建议使用mysqli代替。

Deprecated是什么意思:

这意味着不要使用某些特定的功能/方法/软件功能/特定的软件实践,它只是意味着它不应该被使用,因为该软件中有(或将有)更好的替代方案应该被使用。

使用已弃用的函数时可能会出现几个常见问题:

1.功能完全停止工作:应用程序或脚本可能依赖于不再支持的函数,因此使用它们的改进版本或替代方案。

2.显示关于弃用的警告消息:这些消息通常不会干扰站点功能。但是,在某些情况下,它们可能会破坏服务器发送标头的过程。

例如:这可能会导致登录问题(cookie/会话设置不正确)或转发问题(301/302/303重定向)。

牢记:

-已弃用的软件仍然是软件的一部分。

-已弃用的代码只是代码的状态(标签)。

MYSQL与MYSQLI的主要区别mysql*

  • 旧数据库驱动程序
  • MySQL只能在程序上使用
  • 无法抵御SQL注射攻击
  • 在PHP 5.5.0中已弃用,并在PHP 7中删除

mysqli

  • 新的数据库驱动程序
  • 目前正在使用
  • 准备好的语句可以防止攻击

如果您确定不想升级php版本,则无需更新,但同时您也不会获得安全更新,这将使您的网站更容易受到黑客攻击,这是主要原因。