为什么不应该使用mysql_*
函数?(例如mysql_query()
,mysql_connect()
或mysql_real_escape_string()
)有哪些技术原因?
即使它们在我的网站上运行良好,为什么我还应该使用其他内容?
如果它们不能在我的网站上运行,为什么会出现如下错误:
警告:mysql_connect():没有这个文件或目录
为什么不应该使用mysql_*
函数?(例如mysql_query()
,mysql_connect()
或mysql_real_escape_string()
)有哪些技术原因?
即使它们在我的网站上运行良好,为什么我还应该使用其他内容?
如果它们不能在我的网站上运行,为什么会出现如下错误:
警告:mysql_connect():没有这个文件或目录
MySQL扩展:
由于它已被弃用,使用它会使您的代码不够未来化。
缺乏对预处理语句的支持特别重要,因为它们提供了一种比手动使用单独的函数调用进行转义和引用外部数据更清晰、更少出错的方法。
请参见SQL扩展的比较。
mysql
(在PHP 7中已删除),mysqli
和PDO
扩展。
mysql_*
函数曾经非常流行,但现在不再鼓励使用。文档团队正在讨论数据库安全情况,并教育用户摆脱常用的ext/mysql扩展(请查看php.internals: deprecating ext/mysql)。ext/mysql
不仅仅是为了安全性,还为了能够使用MySQL数据库的所有功能。
ext/mysql
是为MySQL 3.23构建的,自那时以来只有很少的添加,同时大部分保持与这个旧版本的兼容性,这使得代码更难维护。不受ext/mysql
支持的缺失功能包括:(来自PHP手册)。
不使用mysql_*
函数的原因:
特别重要的是,缺乏对预处理语句的支持,因为它们提供了比使用单独的函数调用手动转义和引用外部数据更清晰、更少出错的方法。
请参阅SQL扩展的比较。
error_reporting = E_ALL ^ E_DEPRECATED
PDO
,我现在正在撰写一个简单的PDO
教程。
使用mysql_*
函数或者我们可以说是老方法(在PHP 5.5及以上版本中已弃用)
$link = mysql_connect('localhost', 'user', 'pass');
mysql_select_db('testdb', $link);
mysql_set_charset('UTF-8', $link);
$db = new PDO('mysql:host=localhost;dbname=testdb;charset=utf8', 'username', 'password');
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));
$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);
//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());
}
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
并仅查看错误日志来隐藏危险的错误信息。SELECT
、INSERT
、UPDATE
或DELETE
语句时,这是什么鬼?别担心,我们来看看:
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
<?php
foreach($db->query('SELECT * FROM table') as $row) {
echo $row['field1'];
}
$stmt->fetch(PDO::FETCH_ASSOC)
PDOStatement::fetchAll()
- 返回包含所有结果集行的数组PDOStatement::fetchColumn()
- 从结果集的下一行返回单个列PDOStatement::fetchObject()
- 获取下一行并将其作为对象返回PDOStatement::setFetchMode()
- 设置此语句的默认获取模式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';
<?php
$result = $db->exec("INSERT INTO table(firstname, lastname) VAULES('John', 'Doe')");
$insertId = $db->lastInsertId();
mysql_*
函数中所做的是:<?php
$results = mysql_query("UPDATE table SET field='value'") or die(mysql_error());
echo mysql_affected_rows($result);
<?php
$affected_rows = $db->exec("UPDATE table SET field='value'");
echo $affected_rows;
PDO::exec
执行一个SQL语句并返回受影响的行数。
插入和删除将在后面介绍。
上述方法仅在您不在查询中使用变量时有用。但是,当您需要在查询中使用变量时,千万不要像上面那样尝试,而应使用预处理语句或参数化语句。
问:什么是预编译语句,为什么我需要它们?
答:预编译语句是一种预先编译的SQL语句,可以通过仅向服务器发送数据来多次执行。
使用预编译语句的典型工作流程如下(引用自维基百科的三个要点):
?
):`INSERT INTO PRODUCT (name, price) VALUES (?, ?)`
1.00
。 $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);
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()
方法。
SELECT
、INSERT
、UPDATE
、DELETE
预编译查询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);
INSERT
:
$stmt = $db->prepare("INSERT INTO table(field1,field2) VALUES(:field1,:field2)"); $stmt->execute(array(':field1' => $field1, ':field2' => $field2)); $affected_rows = $stmt->rowCount();
DELETE
:
$stmt = $db->prepare("DELETE FROM table WHERE id=:id"); $stmt->bindValue(':id', $id, PDO::PARAM_STR); $stmt->execute(); $affected_rows = $stmt->rowCount();
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 /*"));
首先,让我们从我们给每个人的标准评论开始:
请不要在新代码中使用
mysql_*
函数。它们已不再得到维护并已正式弃用。看到红框了吗?请了解预处理语句,并使用PDO或MySQLi - 本文将帮助您决定哪种方法更好。如果您选择PDO,则这里有一个很好的教程。
让我们逐句解释一下:
它们不再得到维护,已经被官方弃用
这意味着PHP社区正在逐渐停止支持这些非常老的函数。它们很可能在未来(近期)的PHP版本中不存在!继续使用这些函数可能会在不远的将来破坏您的代码。
新! - ext/mysql现在已经官方弃用自PHP 5.5开始!
相反,您应该学习使用预处理语句
mysql_*
扩展不支持预处理语句,而这是(其他功能之一)针对SQL注入非常有效的对策。它修复了MySQL依赖应用程序中的一个非常严重的漏洞,允许攻击者访问您的脚本并在您的数据库上执行任何可能的查询。
有关更多信息,请参见如何在PHP中防止SQL注入?
看到红框了吗?
当您转到任何mysql
函数手册页面时,您会看到一个红框,解释它不应再使用。
使用PDO或MySQLi
有更好、更健壮和精心构建的替代方案,PDO - PHP数据库对象,它提供了完整的面向对象的数据库交互方式,以及MySQLi,它是针对MySQL的特定改进。
IN (...)
结构的任何有意义的用法。 - Eugen Rieckmysqli_*
的内容,但它仍然很糟糕,因为该实现的结构不好,不应使用。请改用PDO。 - Shadur分析和综合原因已经提到。对于新手来说,停止使用过时的mysql_函数有更重要的动力。
现代数据库API只是更加易于使用。
这主要是由于可以简化代码的绑定参数。并且通过优秀的教程(如上所示),转换为PDO并不过于艰难。
然而,一次性重写较大的代码库需要时间。这就是中间替代方案的存在理由:
使用<pdo_mysql.php>,您可以轻松切换到旧的mysql_函数。它添加了pdo_
函数包装器,用于替换其mysql_
对应项。
Simply include_once(
"pdo_mysql.php"
);
in each invocation script that has to interact with the database.
Remove the function prefix everywhere and replace it with mysql_
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()
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。
现在是时候实际利用它了。
pdo_query()
非常容易支持绑定参数。将旧代码转换很简单:
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)
:named
占位符列表。更重要的是,您可以安全地在任何查询后面传递 $_REQUEST[] 变量。当提交的 <form>
字段与数据库结构完全匹配时,甚至更短:
pdo_query("INSERT INTO pages VALUES (?,?,?,?,?)", $_POST);
非常简单。但让我们回到一些关于重写建议和技术原因的内容,解释为什么您可能希望摆脱 和转义。mysql_
sanitize()
函数一旦您将所有 调用转换为带有绑定参数的 mysql_
pdo_query
,请删除所有冗余的 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_tags
,htmlentities
用于输出上下文,最后才是 _escape_string
,因为它的应用程序应直接在 SQL 中间插入。_real_escape_string
的调用。sanitize()
函数。添加一个注释,表示此后仅适用于 HTML 转义。stripslashes()
,则可能表明存在更高级别的 oversight。magic_quotes
的损坏(双重转义)。但是,这最好集中修复,而不是逐个字符串地修复。sanitize
函数中的 stripslashes()
。当您将字符串变量混合到SQL查询中时,不仅对您而言更加复杂,MySQL再次分离代码和数据也是多余的工作。
mysql_
pdo_*
包装器函数提供了一个编码友好的临时 API。(如果没有特异功能的函数签名转换,它基本上就是 MYSQLI
)。它们还在大多数时候暴露真实的 PDO。$result = pdo_query("SELECT * FROM tbl");
while ($row = pdo_fetch_assoc($result)) {
可以用 foreach 循环代替:
foreach ($result as $row) {
或者更好的是直接和完整地检索数组:
$result->fetchAll();
在大多数情况下,与PDO或mysql_在查询失败后提供的警告相比,您将获得更多有用的警告。
因此,这些实际原因和值得采取的途径希望可以说明放弃的实际意义。mysql_
仅仅切换到pdo还不够。 pdo_query()
也只是其前端。
除非您还引入参数绑定或可以利用较好的API中的其他内容,否则这是一个无意义的转换。我希望它足够简单,不会进一步使新手受挫。(教育通常比禁令更有效。)
虽然它符合可能起作用的最简单事物类别,但它仍然是非常实验性的代码。我只是在周末写了它。然而,有很多替代方案。只需搜索PHP数据库抽象并浏览一下即可找到许多优秀的库来处理此类任务。
如果你想进一步简化数据库交互,像Paris/Idiorm这样的映射器是值得一试的。就像没有人再使用单调的JavaScript DOM一样,现在你也不必再照看原始的数据库接口了。pdo_query("INSERT INTO pages VALUES (?,?,?,?,?)", $_POST);
函数 - 例如: `pdo_query("INSERT INTO users VALUES (?, ?, ?), $_POST); $_POST = array('username' => 'lawl', 'password' => '123', 'is_admin' => 'true');` - rickyduckpdo_real_escape_string()
<- 这是一个真正的函数吗?我找不到任何关于它的文档。请提供一个来源。 - Ryan Stonemysql_
函数:
mysqli_
。 - eggyalmysql_*
函数是新版本 PHP 中 mysqlnd 函数的外壳。因此,即使旧的客户端库不再维护,mysqlnd 仍然得到维护 :) - hakremysql_query("CALL my_proc");
已经很久了)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?
嗯,答案如下:
如果像绝大多数 PHP 开发者一样,在应用程序代码中直接使用原始 API 调用(这本质上是错误的做法),则 PDO 是唯一的选择,因为这个扩展程序不仅是 API,而是半 DAL,虽然还不完整,但提供了许多重要特性,其中两个使 PDO 与 mysqli 显著不同:
所以,如果您是一名普通的 PHP 用户,并且希望在使用原生预处理语句时节省大量麻烦,PDO 再次是唯一的选择。
但是,PDO 也不是万能药,并且有其困难之处。
因此,我在 PDO 标签维基 中编写了所有常见陷阱和复杂案例的解决方案。
尽管如此,每个谈论扩展程序的人总是忽略了两个关于 Mysqli 和 PDO 的重要事实:
准备语句并不是万能的。有无法使用准备语句绑定的动态标识符。有具有未知参数数量的动态查询,这使得查询构建成为一个困难的任务。
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处理这种实际情况的例子。
而且这样会太冗长,很可能不安全。
所以,再一次强调 - 不仅应该关注原始驱动程序,还需要关注抽象类,它不仅对初学者手册中的愚蠢示例有用,还可以解决任何现实生活中遇到的问题。
mysql_*
函数使漏洞变得非常容易出现。由于PHP被许多新手用户使用,实际上在实践中使用mysql_*
甚至是有害的,即使从理论上讲,也可能没有问题。 - Madara's Ghost有很多原因,但可能最重要的原因是这些函数鼓励不安全的编程实践,因为它们不支持准备好的语句。准备好的语句有助于防止SQL注入攻击。
使用mysql_*
函数时,您必须记住通过mysql_real_escape_string()
运行用户提供的参数。如果您只忘记在一个地方进行转义,或者偶然只对输入的一部分进行转义,那么您的数据库可能会受到攻击。
在PDO
或mysqli
中使用准备好的语句将使这些编程错误更难发生。
因为(除了其他原因之外),确保输入数据经过净化变得更加困难。如果使用PDO或mysqli等参数化查询,可以完全避免这种风险。
举个例子,有人可能会将"enhzflep); drop table users"
作为用户名。旧的函数允许在一个查询中执行多个语句,所以像那个令人讨厌的家伙一样的东西可以删除整个表。
如果使用PDO或mysqli,用户名最终将成为"enzhflep); drop table users"
。
请参见bobby-tables.com。
mysqli_multi_query()
函数。可以通过ext/mysql和未转义的字符串进行的注入攻击是像' OR '1' = '1
这样的语句,用于提取不应被访问的数据库数据。在某些情况下,可以注入子查询,但仍然无法以此方式修改数据库。 - DaveRandom本答案旨在展示如何绕过编写不良的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...
你被喊叫的原因是发帖提问时,人们能够看到你的代码可以轻易地被绕过。请使用这个问答来改进你的代码,使其更加安全,并使用当前的函数。
最后,这并不是说这是完美的代码。你可以做很多事情来改进它,例如使用散列密码,确保在数据库中存储敏感信息时,不要以明文形式存储它,有多个验证级别等等。但如果你仅将旧的注入易受攻击的代码更改为此代码,你就已经朝着编写良好代码的方向迈出了很大的一步。而且你已经到了这个地步并且仍在阅读,这给我带来了希望,希望你不仅在编写网站和应用程序时实施这种类型的代码,而且还能去研究我刚才提到的其他事情——以及更多。尽你所能编写最好的代码,而不是只能勉强运行的基本代码。
mysql_*
本身并不是不安全的,但是由于糟糕的教程和缺乏适当的语句准备 API,它会促使编写不安全的代码。 - Madara's Ghost