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


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

即使他们在我的网站上工作,为什么还要使用别的东西?

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

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


2196
2017-10-12 13:18


起源


错误如下:致命错误:未捕获错误:调用未定义函数mysql_connect()... - Bimal Poudel
单独弃用是足以避免它们的原因 - Sasa1234


答案:


MySQL扩展:

  • 不是在积极发展
  • 正式 弃用 自PHP 5.5起(2013年6月发布)。
  • 一直 去除 完全 自PHP 7.0起(2015年12月发布)
    • 这意味着,作为 2018年12月31日 它不会存在于任何受支持的PHP版本中。目前,它只会得到 安全 更新。
  • 缺少OO界面
  • 不支持:
    • 非阻塞,异步查询
    • 准备好的陈述 或参数化查询
    • 存储过程
    • 多个陈述
    • 交易
    • “新”密码验证方法(MySQL 5.6中默认启用; 5.7中要求)
    • MySQL 5.1中的所有功能

由于它已被弃用,因此使用它会使您的代码不再适用于未来。

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

看到 SQL扩展的比较


1814
2018-01-01 11:52



单独弃用是足以避免它们的原因。有一天他们不会在那里,如果你依赖它们,你就不会高兴。其余的只是一个使用旧扩展程序阻止人们学习的东西列表。 - Tim Post♦
贬值并不是每个人都认为的神奇子弹。 PHP本身有一天不会存在,但我们依赖于我们现在拥有的工具。当我们必须改变工具时,我们会。 - Lightness Races in Orbit
@LightnessRacesinOrbit - 弃用不是一个神奇的子弹,它是一个标志,上面写着“我们认识到这很糟糕,所以我们不会再支持它了”。虽然有更好的未来代码验证是远离已弃用功能的一个很好的理由,但它不是唯一的(甚至是主要的)。改变工具因为有更好的工具,而不是因为你被迫。 (并且在你被迫之前更换工具意味着你没有学习新的工具只是因为你的代码已经停止工作并且需要在昨天修复...这是学习新工具的最糟糕时间)。 - Quentin
关于缺乏准备好的陈述,我没有提到的一件事是性能问题。每次发表声明时 某物 必须编译它,以便MySQL守护程序可以理解它。使用此API,如果您在循环中发出200,000个相同的查询,那么必须为MySQL编译200,000次查询才能理解它。使用预准备语句,将其编译一次,然后将值参数化到已编译的SQL中。 - Goldentoa11
@symcbean,肯定会 不 支持预备陈述。这实际上是它被弃用的主要原因。如果没有(易于使用)预处理语句,mysql扩展通常会成为SQL注入攻击的牺牲品。 - rustyx


PHP提供了三种不同的API来连接MySQL。这些是 mysql(从PHP 7开始删除), mysqli,和 PDO 扩展。

mysql_* 以前的功能很受欢迎,但不再鼓励它们的使用。文档团队正在讨论数据库安全情况,并教育用户远离常用的ext / mysql扩展是其中的一部分(检查 php.internals:弃用ext / mysql)。

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

ext/mysql 是 自PHP 5.5起正式弃用 并且一直 从PHP 7开始删除

看红盒子?

当你继续下去 mysql_* 功能手册页,你看到一个红色的框,解释它不应该再使用。

为什么


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

ext/mysql 是为...而建的 MySQL 3.23 从那以后只有很少的添加,同时大部分保持与这个旧版本的兼容性,这使代码有点难以维护。缺少不受支持的功能 ext/mysql 包括:(来自PHP手册)。

不使用的理由 mysql_* 功能

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

以上点引用了昆汀的回答

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

SQL扩展的比较


抑制弃用警告

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

error_reporting = E_ALL ^ E_DEPRECATED

请注意,这也将隐藏 其他弃用警告但是,它可能用于除MySQL之外的其他东西。 (来自PHP手册

这篇文章 PDO与MySQLi:你应该使用哪个? 通过 德扬马里亚诺维奇 将帮助您选择。

更好的方法是 PDO,我现在写的很简单 PDO 教程。


一个简单而简短的PDO教程


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

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

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 (数据源名称)和可选 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 进一步。

好读连接和连接管理¶ 

您还可以将多个驱动程序选项作为数组传递给第四个参数。我建议传递put的参数 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构建后设置属性吗?

,我们也可以在PDO构造之后设置一些属性 setAttribute 方法:

$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 off 并只是阅读你的错误日志。

现在,在阅读了上述所有内容之后,你可能会想:当我只是想开始简单的时候,那是什么 SELECTINSERTUPDATE, 要么 DELETE 声明?别担心,我们走了:


选择数据

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你想要获取数据的方式。在下面的部分我将解释这一点。

获取模式

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

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

 $stmt->fetch(PDO::FETCH_ASSOC)

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

现在我来获取模式:

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

还有更多的选择!全部了解它们 PDOStatement 获取文档。

获取行数

而不是使用 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语句并返回受影响的行数。

稍后将介绍插入和删除。

只有在查询中不使用变量时,上述方法才有用。但是当你需要在查询中使用变量时,不要尝试像上面那样 准备好的声明或参数化声明 是。


准备好的陈述

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

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

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

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

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

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

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

Q. 那么现在,什么是命名占位符以及如何使用它们?
一个。 命名占位符。使用以冒号开头的描述性名称,而不是问号。我们不关心名称持有人的价值位置/顺序:

 $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. 那么现在,什么是未命名的占位符以及如何使用它们?
一个。 我们举个例子:

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

1163
2017-10-12 13:28



上面的好读应该提到:准备好的陈述带走任何有意义的使用 IN (...) construct。 - Eugen Rieck
@Amine,不,不是! :]虽然 NullPoiиteя 写得真的很棒,这绝对不是一个好的阅读,因为它是漫长的方式。我很确定,10个访客中有8个会跳过它。你也有解释,为什么这个答案不是最高票。一个 tl;dr 我认为,一开始部分是一个好主意。 - trejder
问题是“为什么我不能在PHP中使用mysql_ *函数”。这个答案虽然令人印象深刻并且充满了有用的信息,但仍然超出了范围,就像@trejder所说 - 10个人中有8个人会错过这些信息,因为他们没有4个小时的时间来完成工作它。这将是更有价值的分解,并用作几个更精确的问题的答案。 - Alex McMillan
Persoanlly我更喜欢mysqli和PDO。但对于模具处理,我尝试了异常替代方案 function throwEx() { throw new Exception("You did selected not existng db"); } mysql_select_db("nonexistdb") or throwEx(); 它适用于抛出异常。 - kuldeep.kamboj


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

请不要使用 mysql_* 新代码中的函数。他们不再维护 并且正式弃用。见 红色的盒子?学习关于 准备好的陈述 相反,并使用 PDO 要么 库MySQLi  - 本文 将帮助您决定哪个。如果你选择PDO, 这是一个很好的教程

让我们逐句逐句解释:

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

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

    新! - 现在是ext / mysql 自PHP 5.5起正式弃用!

    较新的! EXT / MySQL的 已在PHP 7中删除

  • 相反,你应该学习准备好的陈述

    mysql_* 扩展不支持 准备好的陈述,这是(除其他外)一个非常有效的对策 SQL注入。它修复了MySQL依赖应用程序中的一个非常严重的漏洞,它允许攻击者获得对脚本的访问权限并执行 任何可能的查询 在您的数据库上。

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

  • 看红盒子?

    当你去任何 mysql 功能手册页,你看到一个红色的框,解释它不应该再使用。

  • 使用PDO或MySQLi

    有更好,更强大和精心打造的替代品, PDO - PHP数据库对象,它提供了完整的OOP方法来进行数据库交互,以及 库MySQLi,这是MySQL特定的改进。


280
2017-12-24 23:30



还有一件事:我认为这个功能仍然存在于PHP只有一个原因 - 兼容旧的,过时但仍在运行的CMS,电子商务,公告板系统等。最后它将被删除,你将不得不重写你的应用... - Kamil
@Kamil:这是真的,但这并不是你不应该使用它的原因。不使用它的原因是因为它古老,不安全等等:) - Madara Uchiha♦
@Mario - PHP开发人员确实有一个进程,他们刚刚投票支持从5.5开始正式弃用ext / mysql。这不再是一个假设的问题。 - SDC
使用经过验证的技术(如PDO或MySQLi)添加一些额外的行仍然提供了PHP一直提供的易用性。我希望为了开发人员,他/她知道在任何教程中看到这些神奇的mysql_ *函数实际上都会减损这一课程,并且应该告诉OP这种代码在10年前是如此 - 并且应该质疑教程的相关性也是如此! - FredTheWebGuy
答案应该恰当地提到:准备好的陈述带走任何有意义的使用 IN (...) construct。 - Eugen Rieck


使用方便

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

当代数据库API就是 更轻松 使用。

这主要是 绑定参数 这可以简化代码。与 优秀的教程(如上所示) 过渡到 PDO 并不过分艰苦。

一次重写更大的代码库需要时间。 Raison d'être为这个中间选择:

等效的pdo_ *函数代替 mysql_ *

运用 <pdo_mysql.php> 你可以用旧的mysql_函数切换 最小的努力。它补充道 pdo_ 功能包装器替换他们的 mysql_ 同行。

  1. 只是 include_once("pdo_mysql.php"); 在每个必须与数据库交互的调用脚本中。

  2. 去除 mysql_ 功能前缀 到处 并替换它 pdo_

    • mysql_connect() 变 pdo_connect()
    • mysql_query() 变 pdo_query()
    • mysql_num_rows() 变 pdo_num_rows()
    • mysql_insert_id() 变 pdo_insert_id()
    • mysql_fetch_array() 变 pdo_fetch_array()
    • mysql_fetch_assoc() 变 pdo_fetch_assoc()
    • mysql_real_escape_string() 变 pdo_real_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]";
    }
    

Etvoilà。
你的代码是 运用 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_和逃避。

修复或删除任何oldschool sanitize() 功能

一旦你转换了全部 mysql_ 打电话给 pdo_query 绑定params,删除所有冗余 pdo_real_escape_string 调用。

特别是你应该解决任何问题 sanitize 要么 clean 要么 filterThis 要么 clean_data 功能由日期教程以一种形式或另一种形式宣传:

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

这里最明显的错误是缺乏文档。更重要的是,过滤顺序完全错误。

  • 正确的顺序是:弃用 stripslashes 那么,作为最里面的呼唤 trim之后 strip_tagshtmlentities 对于输出上下文,最后只有 _escape_string 因为它的应用程序应该直接在SQL intersparsing之前。

  • 但作为第一步 摆脱了 _real_escape_string 呼叫。

  • 您可能需要保留其余的 sanitize() 如果您的数据库和应用程序流期望HTML上下文安全的字符串,则现在运行。添加一条注释,它仅适用于今后的HTML转义。

  • 字符串/值处理委托给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变量值。

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

请注意参数绑定仍然不是一个神奇的一站式解决方案 所有 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中的其他内容,否则它是一个毫无意义的开关。我希望它的描述足够简单,不会进一步阻碍新人的沮丧。 (教育通常比禁止更好。)

虽然它有资格获得最简单的可能工作类别,但它仍然是非常实验性的代码。我只是在周末写的。然而,有许多替代品。只是google for PHP数据库抽象 并浏览一下。对于这样的任务,总会有很多优秀的库。

如果你想进一步简化你的数据库交互,那么mappers就像 巴黎/ Idiorm 值得一试。就像没有人再使用JavaScript中的平淡DOM一样,你现在不必保持原始的数据库接口。


201
2017-10-12 13:22



小心了 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是最后一次),但你可以创建一个 化石帐户,添加到维基或文件 错误报告 (没有注册IIRC)。 - mario


mysql_ 功能:

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

136
2018-01-01 17:42



#2同样如此 mysqli_ - eggyal
公平地说,鉴于SQL方言的变化,即使PDO也没有给你任何程度的确定性。你需要一个合适的ORM包装器。 - SDC
@SDC确实 - 标准的问题在于 所以 许多人... - Alnitak
xkcd.com/927 - Lightness Races in Orbit
该 mysql_* 函数是用于较新PHP版本的mysqlnd函数的shell。所以,即使旧的客户端库不再维护,也保持mysqlnd :) - hakre


说起 技术 原因,只有少数,非常具体,很少使用。很可能你永远不会在生活中使用它们。
也许我太无知了,但我从未有机会使用它们之类的东西

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

如果你需要它们 - 这些毫无疑问是技术上的原因,从mysql扩展转向更时尚和现代化的东西。

然而,也存在一些非技术性问题,这些问题可能会让您的体验更加艰难

  • 在现代PHP版本中进一步使用这些函数将引发弃用级别的通知。他们只是可以关闭。
  • 在遥远的未来,它们可能会从默认的PHP构建中删除。也没什么大不了的,因为mydsql ext将被转移到PECL中,每个主机都很乐意用它编译PHP,因为他们不想失去那些网站工作了几十年的客户。
  • 来自Stackoverflow社区的强烈抵制。 Еverytime你提到这些诚实的功能,你被告知他们是严格的禁忌。
  • 作为普通的PHP用户,很可能您使用这些功能的想法容易出错并且错误。正因为所有这些教程和手册教你错误的方法。不是功能本身 - 我必须强调它 - 但它们的使用方式。

后一个问题是一个问题。
但是,在我看来,提出的解决方案也不是更好。
在我看来 太理想化了 梦想所有这些PHP用户将学习如何正确处理SQL查询。很可能他们只是机械地将mysql_ *改为mysqli_ *, 离开方法是一样的。特别是因为mysqli使准备好的语句使用令人难以置信的痛苦和麻烦。
更不用说了 本地人 准备好的陈述 不足以保护 从SQL注入开始,mysqli和PDO都没有提供解决方案。

所以,与其打击这种诚实的延伸,我宁愿采取错误的做法,并以正确的方式教育人们。

此外,有一些错误或非重要的原因,如

  • 不支持存储过程(我们正在使用 mysql_query("CALL my_proc"); 很长时间)
  • 不支持交易(与上述相同)
  • 不支持多个语句(谁需要它们?)
  • 没有积极的发展(那么它有什么影响  以任何实际的方式?)
  • 缺少OO界面(创建一个是几个小时的事情)
  • 不支持Prepared Statements或Parametrized Queries

最后一个是有趣的一点。虽然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标签维基

尽管如此,每个谈论扩展的人总是错过了 2个重要事实 关于Mysqli和PDO:

  1. 准备好的声明 不是银弹。有动态标识符不能使用预准备语句绑定。存在具有未知数量的参数的动态查询,这使得查询构建成为困难的任务。

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

所以,仅仅切换到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处理这种实际案例的例子。
而且它太过于罗嗦而且很可能不安全。

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


99
2017-10-12 13:23



mysql_* 使漏洞变得非常容易。由于PHP被很多新手用户使用, mysql_* 在实践中是积极有害的,即使理论上它可以毫无障碍地使用。 - Madara Uchiha♦
everything is parameterized and safe - 它可能是参数化的,但您的功能不使用 真实 准备好的陈述 - uınbɐɥs
怎么 Not under active development 只有那个'0.01%'?如果你使用这个静止功能构建一些东西,在一年内更新你的mysql版本并结束一个非工作系统,我敢肯定有很多人突然在'0.01%'。我想说的是 deprecated 和 not under active development 密切相关。你可以说它没有“没有[值得]理由”,但事实是,当提供选项之间的选择时, no active development 差不多就像 deprecated 我会说? - Nanne
@MadaraUchiha:你能解释一下漏洞是如何容易得到的吗?特别是在那些相同的漏洞不会影响PDO或MySQLi的情况下......因为我不知道你所说的单个漏洞。 - ircmaxell
@ShaquinTrifonoff:当然,它不使用预准备语句。但 PDO也没有,大多数人推荐MySQLi。所以我不确定这会产生重大影响。上面的代码(稍微解析一下)就是PDO默认准备语句时所做的事情...... - ircmaxell


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

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

使用准备好的语句 PDO 要么 mysqli 将使这些编程错误更难以制作。


88
2017-10-12 13:24



不幸的是,MySQLi_ *中用于传递可变数量参数的不良支持(例如当你想传递一个值列表以在IN子句中进行检查时)会鼓励不使用参数,鼓励使用完全相同的连接查询让MySQL_ *调用易受攻击。 - Kickstart
但是,不安全性不再是mysql_ *函数的固有问题,而是一个不正确的使用问题。 - Agamemnus
@Agamemnus问题是mysql_ *可以很容易地实现“不正确的使用”,特别是对于没有经验的程序员。实现预准备语句的库使得更难以产生这种类型的错误。 - Trott


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

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

如果一个人使用mysqli的PDO,那么用户名将最终成为 "enhzflep); drop table users"

看到 bobby-tables.com


71
2017-09-18 12:28



The old functions will allow executing of multiple statements per query  - 不,他们不会。使用ext / mysql无法进行这种注入 - 使用MySQL和MySQL进行这种注入的唯一方法就是使用MySQLi和 mysqli_multi_query() 功能。使用ext / mysql和非转义字符串可以实现类型注入 ' OR '1' = '1 从数据库中提取不可访问的数据。在某些情况下,可以注入子查询,但仍然无法以这种方式修改数据库。 - DaveRandom


这个答案是为了说明绕过写得不好的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: bob
pass: somePass

并提交。

输出如下:

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

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

user: Fluffeh
pass: mypass

惊人!全面的Hi-fives,代码正确验证了管理员。这是完美的!

嗯,不是真的。让我们说用户是一个聪明的小人物。让我们说这个人就是我。

输入以下内容:

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

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

结果太棒了。让我们再次尝试那些用户名和密码组合:

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...

这就是为什么你在发布问题时被大吼大叫的原因 - 这是因为人们可以看到你的代码可以绕过而不用尝试。请使用此问题和答案来改进您的代码,使其更安全并使用当前的功能。

最后,这并不是说这是完美的代码。你还可以做很多事情来改进它,例如使用散列密码,确保当你在数据库中存储感知信息时,你不会以纯文本形式存储它,有多级验证 - 但实际上,如果你只需要改变你的旧注入代码就可以了,在编写优秀代码的过程中你会很好 - 而且你已经掌握了这一点而且还在阅读的事实让我有一种希望,你不仅会实现这种类型在编写您的网站和应用程序时的代码,但您可能会出去研究我刚才提到的其他事情 - 以及更多。编写尽可能最好的代码,而不是几乎不起作用的最基本代码。


63
2017-09-02 07:20



谢谢您的回答!有我的+1!值得注意的是 mysql_* 本身并不是不安全,但它确实通过糟糕的教程和缺乏适当的语句准备API来促进不安全的代码。 - Madara Uchiha♦
没有密码,哦,恐怖! = oP否则+1以获得详细说明。 - cryptic ツ


MySQL扩展是三者中最老的,是开发人员用来与MySQL通信的原始方式。这个扩展现在正在 弃用 赞成对方   备择方案 因为PHP和MySQL的新版本都有所改进。

  • 库MySQLi 是使用MySQL数据库的“改进”扩展。它利用了较新版本的MySQL服务器中提供的功能,向开发人员公开了面向功能的界面和面向对象的界面,并且做了一些其他很好的事情。

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


31
2017-09-07 15:06