问题 如何在PHP中阻止SQL注入?
如果在未修改SQL查询的情况下插入用户输入,则应用程序将变得容易受到攻击 SQL注入,如下例所示:
$unsafe_variable = $_POST['user_input'];
mysql_query("INSERT INTO `table` (`column`) VALUES ('$unsafe_variable')");
这是因为用户可以输入类似的东西 value'); DROP TABLE table;--
,查询变为:
INSERT INTO `table` (`column`) VALUES('value'); DROP TABLE table;--')
可以采取哪些措施来防止这种情况发生?
12070
2019-05-20 20:50:04
起源
答案:
使用预准备语句和参数化查询。 这些是由数据库服务器与任何参数分开发送和解析的SQL语句。这样攻击者就无法注入恶意SQL。
你基本上有两个选项来实现这个目标:
运用 PDO (对于任何支持的数据库驱动程序)
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute(array('name' => $name));
foreach ($stmt as $row) {
// do something with $row
}
运用 库MySQLi (对于MySQL):
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// do something with $row
}
如果您要连接到MySQL以外的数据库,则可以参考特定于驱动程序的第二个选项(例如 pg_prepare()
和 pg_execute()
对于PostgreSQL)。 PDO是通用选项。
正确设置连接
请注意,使用时 PDO
访问MySQL数据库 真实 准备好的陈述是 默认情况下不使用。要解决此问题,您必须禁用预准备语句的模拟。使用PDO创建连接的示例是:
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
在上面的例子中,错误模式并不是绝对必要的, 但建议添加它。这样脚本就不会停止了 Fatal Error
什么时候出错了。它为开发人员提供了机会 catch
任何错误 throw
n as PDOException
秒。
什么是 强制性然而,这是第一个 setAttribute()
line,告诉PDO禁用模拟的预准备语句并使用 真实 准备好的陈述这样可以确保PHP在将语句和值发送到MySQL服务器之前不会对其进行解析(使攻击者可能无法注入恶意SQL)。
虽然你可以设置 charset
在构造函数的选项中,重要的是要注意PHP的“旧”版本(<5.3.6) 默默地忽略了charset参数 在DSN中。
说明
会发生什么是您传递给的SQL语句 prepare
由数据库服务器解析和编译。通过指定参数(a ?
或者像。这样的命名参数 :name
在上面的示例中,您告诉数据库引擎要筛选的位置。然后当你打电话 execute
,准备好的语句与您指定的参数值组合在一起。
这里重要的是参数值与编译语句结合,而不是SQL字符串。 SQL注入通过在创建SQL以发送到数据库时欺骗脚本来包含恶意字符串。因此,通过将参数中的实际SQL分开发送,可以限制结束您不想要的内容的风险。使用预准备语句时发送的任何参数都将被视为字符串(尽管数据库引擎可能会进行一些优化,因此参数最终也可能作为数字)。在上面的例子中,如果 $name
变量包含 'Sarah'; DELETE FROM employees
结果只是搜索字符串 "'Sarah'; DELETE FROM employees"
,你不会最终得到 一张空桌子。
使用预准备语句的另一个好处是,如果在同一个会话中多次执行相同的语句,它只会被解析和编译一次,从而为您带来一些速度提升。
哦,既然你问过如何为插入操作,这是一个例子(使用PDO):
$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');
$preparedStatement->execute(array('column' => $unsafeValue));
准备好的语句可以用于动态查询吗?
虽然您仍然可以为查询参数使用预准备语句,但动态查询本身的结构无法进行参数化,并且某些查询功能无法进行参数化。
对于这些特定方案,最好的办法是使用限制可能值的白名单过滤器。
// Value whitelist
// $dir can only be 'DESC' otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}
7954
答案:
使用预准备语句和参数化查询。 这些是由数据库服务器与任何参数分开发送和解析的SQL语句。这样攻击者就无法注入恶意SQL。
你基本上有两个选项来实现这个目标:
运用 PDO (对于任何支持的数据库驱动程序)
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->execute(array('name' => $name));
foreach ($stmt as $row) {
// do something with $row
}
运用 库MySQLi (对于MySQL):
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name); // 's' specifies the variable type => 'string'
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// do something with $row
}
如果您要连接到MySQL以外的数据库,则可以参考特定于驱动程序的第二个选项(例如 pg_prepare()
和 pg_execute()
对于PostgreSQL)。 PDO是通用选项。
正确设置连接
请注意,使用时 PDO
访问MySQL数据库 真实 准备好的陈述是 默认情况下不使用。要解决此问题,您必须禁用预准备语句的模拟。使用PDO创建连接的示例是:
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
在上面的例子中,错误模式并不是绝对必要的, 但建议添加它。这样脚本就不会停止了 Fatal Error
什么时候出错了。它为开发人员提供了机会 catch
任何错误 throw
n as PDOException
秒。
什么是 强制性然而,这是第一个 setAttribute()
line,告诉PDO禁用模拟的预准备语句并使用 真实 准备好的陈述这样可以确保PHP在将语句和值发送到MySQL服务器之前不会对其进行解析(使攻击者可能无法注入恶意SQL)。
虽然你可以设置 charset
在构造函数的选项中,重要的是要注意PHP的“旧”版本(<5.3.6) 默默地忽略了charset参数 在DSN中。
说明
会发生什么是您传递给的SQL语句 prepare
由数据库服务器解析和编译。通过指定参数(a ?
或者像。这样的命名参数 :name
在上面的示例中,您告诉数据库引擎要筛选的位置。然后当你打电话 execute
,准备好的语句与您指定的参数值组合在一起。
这里重要的是参数值与编译语句结合,而不是SQL字符串。 SQL注入通过在创建SQL以发送到数据库时欺骗脚本来包含恶意字符串。因此,通过将参数中的实际SQL分开发送,可以限制结束您不想要的内容的风险。使用预准备语句时发送的任何参数都将被视为字符串(尽管数据库引擎可能会进行一些优化,因此参数最终也可能作为数字)。在上面的例子中,如果 $name
变量包含 'Sarah'; DELETE FROM employees
结果只是搜索字符串 "'Sarah'; DELETE FROM employees"
,你不会最终得到 一张空桌子。
使用预准备语句的另一个好处是,如果在同一个会话中多次执行相同的语句,它只会被解析和编译一次,从而为您带来一些速度提升。
哦,既然你问过如何为插入操作,这是一个例子(使用PDO):
$preparedStatement = $db->prepare('INSERT INTO table (column) VALUES (:column)');
$preparedStatement->execute(array('column' => $unsafeValue));
准备好的语句可以用于动态查询吗?
虽然您仍然可以为查询参数使用预准备语句,但动态查询本身的结构无法进行参数化,并且某些查询功能无法进行参数化。
对于这些特定方案,最好的办法是使用限制可能值的白名单过滤器。
// Value whitelist
// $dir can only be 'DESC' otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}
7954
警告:
这个答案的示例代码(如问题的示例代码)使用PHP mysql
扩展,在PHP 5.5.0中已弃用,并在PHP 7.0.0中完全删除。
如果您使用的是最新版本的PHP,那么 mysql_real_escape_string
下面列出的选项将不再可用(但是 mysqli::escape_string
是一个现代的等价物)。这几天了 mysql_real_escape_string
选项只适用于旧版PHP上的遗留代码。
你有两个选择 - 逃避你的特殊角色 unsafe_variable
,或使用参数化查询。两者都可以保护您免受SQL注入。参数化查询被认为是更好的做法,但在使用之前需要在PHP中更改为更新的mysql扩展。
我们将首先覆盖较低影响的字符串。
//Connect
$unsafe_variable = $_POST["user-input"];
$safe_variable = mysql_real_escape_string($unsafe_variable);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
//Disconnect
另见,细节 mysql_real_escape_string
功能。
要使用参数化查询,您需要使用 库MySQLi 而不是 MySQL的 功能。要重写您的示例,我们需要类似以下内容。
<?php
$mysqli = new mysqli("server", "username", "password", "database_name");
// TODO - Check that connection was successful.
$unsafe_variable = $_POST["user-input"];
$stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");
// TODO check that $stmt creation succeeded
// "s" means the database expects a string
$stmt->bind_param("s", $unsafe_variable);
$stmt->execute();
$stmt->close();
$mysqli->close();
?>
你想要阅读的关键功能就是 mysqli::prepare
。
此外,正如其他人所建议的那样,您可能会发现使用类似的东西来增加抽象层是有用的/更容易的 PDO。
请注意,您询问的案例非常简单,更复杂的案例可能需要更复杂的方法。尤其是:
- 如果要根据用户输入更改SQL的结构,参数化查询将无济于事,并且不需要转义所需的转义
mysql_real_escape_string
。在这种情况下,您最好通过白名单传递用户的输入,以确保只允许“安全”值。
- 如果你在一个条件中使用来自用户输入的整数并采取
mysql_real_escape_string
方法,你会遇到所描述的问题 多项式 在下面的评论中。这种情况比较棘手,因为整数不会被引号括起来,所以你可以通过验证用户输入只包含数字来处理。
- 可能还有其他我不知道的情况。你可能会发现 这个 对于您可能遇到的一些更微妙的问题,它是一个有用的资源。
1514
这里的每个答案仅涵盖部分问题。
事实上,有 四 我们可以动态添加的不同查询部分:
准备好的报表只涵盖其中的两个
但有时我们必须使查询更加动态,同时添加运算符或标识符。
因此,我们需要不同的保护技术。
通常,这种保护方法是基于 白名单。
在这种情况下,每个动态参数都应该在脚本中进行硬编码,并从该集合中进行选择。
例如,要进行动态排序:
$orders = array("name","price","qty"); //field names
$key = array_search($_GET['sort'],$orders)); // see if we have such a name
$orderby = $orders[$key]; //if not, first one will be set automatically. smart enuf :)
$query = "SELECT * FROM `table` ORDER BY $orderby"; //value is safe
但是,还有另一种方法来保护标识符 - 转义。只要你有一个引用的标识符,你可以通过加倍来逃避内部的反引号。
作为进一步的步骤,我们可以从准备好的语句中借用一些占位符(代表查询中的实际值的代理),并发明另一种类型的占位符 - 标识符占位符。
因此,长话短说:这是一个 占位符不是 准备好的声明 可以被视为银弹。
因此,一般性建议可以表述为
只要您使用占位符向查询添加动态部分(当然这些占位符已正确处理),您可以确保查询是安全的。
但是,SQL语法关键字存在问题(例如 AND
, DESC
等等,但白名单似乎是这种情况下唯一的方法。
更新
虽然对SQL注入保护的最佳实践有一个普遍的一致意见,但也有 还有很多不好的做法。 其中一些根深蒂固的PHP用户心中。例如,在这个页面上有(虽然对大多数访问者来说是不可见的) 超过80个删除的答案 - 由于质量差或促进不良和过时的做法,所有被社区删除。更糟糕的是,一些不好的答案不是删除而是繁荣。
例如, 有(1) 是(2) 仍然(3) 许多(4) 答案(5), 包含 第二个最受欢迎的答案 建议你手动字符串转义 - 一种被证明是不安全的过时方法。
或者有一个稍微好一点的答案 另一种字符串格式化方法 甚至称它为终极灵丹妙药。当然,事实并非如此。这种方法并不比常规字符串格式更好,但它保留了所有缺点:它仅适用于字符串,并且与任何其他手动格式一样,它基本上是可选的,非强制性的度量,容易出现任何类型的人为错误。
我认为所有这一切都是因为一个非常古老的迷信,得到了诸如此类机构的支持 OWASP 要么 PHP手册,它宣称任何“逃避”和防止SQL注入之间的平等。
无论PHP手册多年来所说的是什么, *_escape_string
决不会使数据安全 从来没有打算过。除了对字符串以外的任何SQL部分无用之外,手动转义是错误的,因为它是手动的,与自动化相反。
OWASP使情况变得更糟,强调逃避 用户输入 这完全是胡说八道:在注射保护的背景下应该没有这样的词。每个变量都有潜在危险 - 无论来源如何!或者,换句话说 - 每个变量都必须正确格式化以便放入查询中 - 无论来源是什么。这是重要的目的地。在开发人员开始将绵羊与山羊分开的那一刻(想想某个特定变量是否“安全”),他迈出了迈向灾难的第一步。更不用说即使是措辞也表明在入口点大量逃避,类似于非常神奇的引用功能 - 已经被鄙视,弃用和删除。
所以,不像任何“逃避”,准备好的陈述 是 确实可以防止SQL注入的措施(适用时)。
如果你仍然不相信,这是我写的一步一步的解释, Hitchhiker的SQL注入预防指南在那里我详细解释了所有这些问题,甚至编写了一个完全致力于不良做法及其披露的部分。
946
我建议使用 PDO (PHP数据对象)运行参数化SQL查询。
这不仅可以防止SQL注入,还可以加快查询速度。
而使用PDO而不是 mysql_
, mysqli_
,和 pgsql_
函数,您可以使您的应用程序从数据库中抽象出来,在极少数情况下您必须切换数据库提供程序。
770
使用 PDO
和准备好的查询。
($conn
是一个 PDO
目的)
$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();
567
如您所见,人们建议您最多使用预准备语句。这没错,但是当你的查询被执行时 就一次 每个过程,会有轻微的性能损失。
我正面临着这个问题,但我想我已经解决了这个问题 非常 复杂的方式 - 黑客用来避免使用引号的方式。我将它与模拟的预处理语句结合使用。我用它来防止 所有 各种可能的SQL注入攻击。
我的方法:
如果您希望输入为整数,请确保它是 真 整数。在PHP这样的变量类型语言中就是这样 非常 重要。您可以使用这个非常简单但功能强大的解决方案: sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);
如果你期望整数的其他东西 六角形。如果你将它变成十六进制,你将完全逃脱所有输入。在C / C ++中有一个叫做的函数 mysql_hex_string()
,在PHP中你可以使用 bin2hex()
。
不要担心转义字符串的原始长度是原始长度的2倍,因为即使你使用它也是如此 mysql_real_escape_string
,PHP必须分配相同的容量 ((2*input_length)+1)
,这是一样的。
传输二进制数据时经常使用这种十六进制方法,但我认为没有理由不在所有数据上使用它来防止SQL注入攻击。请注意,您必须使用前置数据 0x
或使用MySQL功能 UNHEX
代替。
所以,例如,查询:
SELECT password FROM users WHERE name = 'root'
会变成:
SELECT password FROM users WHERE name = 0x726f6f74
要么
SELECT password FROM users WHERE name = UNHEX('726f6f74')
Hex是完美的逃脱。无法注射。
UNHEX函数和0x前缀之间的区别
评论中有一些讨论,所以我最后要说清楚。这两种方法非常相似,但它们在某些方面略有不同:
** 0x **前缀只能用于数据列,例如 char,varchar,text,block,binary等。
此外,如果您要插入空字符串,它的使用有点复杂。你必须完全替换它 ''
,否则你会收到错误。
UNHEX() 继续努力 任何 柱;你不必担心空字符串。
Hex方法通常用作攻击
请注意,此十六进制方法通常用作SQL注入攻击,其中整数就像字符串一样,只是转义为 mysql_real_escape_string
。然后你可以避免使用引号。
例如,如果您只是这样做:
"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])
攻击可以给你注入 容易。考虑从脚本返回的以下注入代码:
SELECT ... WHERE id = -1 union all来自information_schema.tables的select table_name
现在只需提取表结构:
SELECT ... WHERE id = -1 union all table_schema.column中select table_name,其中table_name = 0x61727469636c65
然后只需选择想要的任何数据。不是很酷吗?
但是如果可注射部位的编码器会使其十六进制,则无法进行注射,因为查询看起来像这样: SELECT ... WHERE id = UNHEX('2d312075...3635')
501
重要
防止SQL注入的最佳方法是使用 准备好的陈述 而不是逃避作为 接受的答案 演示。
有像这样的图书馆 Aura.Sql 和 EasyDB 允许开发人员更容易地使用预准备语句。要了解更多有关准备好的陈述更好的原因 停止SQL注入,参考 这个 mysql_real_escape_string()
旁路 和 最近修复了WordPress中的Unicode SQL Injection漏洞。
注射预防 - mysql_real_escape_string()
PHP有一个特殊的功能来防止这些攻击。你需要做的只是使用一口功能, mysql_real_escape_string
。
mysql_real_escape_string
获取将在MySQL查询中使用的字符串,并返回相同的字符串,并安全地转义所有SQL注入尝试。基本上,它将取代用户可能使用MySQL安全替代品输入的那些麻烦的引号('),一个转义引用\'。
注意: 您必须连接到数据库才能使用此功能!
//连接MySQL
$name_bad = "' OR 1'";
$name_bad = mysql_real_escape_string($name_bad);
$query_bad = "SELECT * FROM customers WHERE username = '$name_bad'";
echo "Escaped Bad Injection: <br />" . $query_bad . "<br />";
$name_evil = "'; DELETE FROM customers WHERE 1 or username = '";
$name_evil = mysql_real_escape_string($name_evil);
$query_evil = "SELECT * FROM customers WHERE username = '$name_evil'";
echo "Escaped Evil Injection: <br />" . $query_evil;
您可以在中找到更多详细信息 MySQL - SQL注入预防。
455
安全警告:这个答案不符合安全最佳实践。 转义不足以阻止SQL注入, 使用 准备好的陈述 代替。使用下面列出的策略需要您自担风险。 (也, mysql_real_escape_string()
在PHP 7中删除了。)
你可以做一些基本的事情:
$safe_variable = mysql_real_escape_string($_POST["user-input"]);
mysql_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
这不会解决所有问题,但它是一个非常好的垫脚石。我省略了明显的项目,例如检查变量的存在,格式(数字,字母等)。
413
无论您最终使用什么,请确保检查您的输入是否已被破坏 magic_quotes
或者其他一些善意的垃圾,如果有必要的话,可以通过它 stripslashes
或者其他什么来消毒它。
347
参数化查询和输入验证是要走的路。尽管如此,在许多情况下可能会发生SQL注入 mysql_real_escape_string()
已经用过。
这些示例容易受到SQL注入:
$offset = isset($_GET['o']) ? $_GET['o'] : 0;
$offset = mysql_real_escape_string($offset);
RunQuery("SELECT userid, username FROM sql_injection_test LIMIT $offset, 10");
要么
$order = isset($_GET['o']) ? $_GET['o'] : 'userid';
$order = mysql_real_escape_string($order);
RunQuery("SELECT userid, username FROM sql_injection_test ORDER BY `$order`");
在这两种情况下,您都无法使用 '
保护封装。
资源: 意外的SQL注入(当转义不够时)
330
在我看来,在PHP应用程序(或任何Web应用程序)中通常阻止SQL注入的最佳方法是考虑应用程序的体系结构。如果防止SQL注入的唯一方法是记住每次与数据库通信时都使用一个特殊的方法或函数来做正确的事情,那么你做错了。这样,你忘记在代码中的某个时刻忘记正确格式化查询只是时间问题。
采用MVC模式和类似的框架 CakePHP的 要么 笨 可能是正确的方法:创建安全数据库查询等常见任务已经解决并在此类框架中集中实施。它们可以帮助您以合理的方式组织Web应用程序,并使您更多地考虑加载和保存对象,而不是安全地构建单个SQL查询。
281