如何防止 PHP 中的 SQL 注入?
-
09-06-2019 - |
题
如果用户输入未经修改就插入到 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;--')
可以采取什么措施来防止这种情况发生?
解决方案
使用准备好的语句和参数化查询。 这些是与任何参数分开发送到数据库服务器并由数据库服务器解析的 SQL 语句。这样攻击者就不可能注入恶意SQL。
基本上你有两种选择来实现这一目标:
使用 磷酸二氢钾 (对于任何受支持的数据库驱动程序):
$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
为 PDOException
s。
什么是 强制的, ,然而,是第一个 setAttribute()
行,告诉 PDO 禁用模拟准备好的语句并使用 真实的 准备好的陈述。这可以确保语句和值在发送到 MySQL 服务器之前不会被 PHP 解析(让可能的攻击者没有机会注入恶意 SQL)。
虽然您可以设置 charset
在构造函数的选项中,重要的是要注意 PHP 的“旧”版本(< 5.3.6) 默默地忽略了 charset 参数 在 DSN 中。
解释
发生的情况是您传递给的 SQL 语句 prepare
由数据库服务器解析和编译。通过指定参数(或者 ?
或命名参数,例如 :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';
}
其他提示
已弃用警告:这个答案的示例代码(就像问题的示例代码一样)使用 PHP
MySQL
扩展,该扩展在 PHP 5.5.0 中已弃用,并在 PHP 7.0.0 中完全删除。安全警告:这个答案不符合安全最佳实践。 转义不足以防止 SQL 注入, , 使用 准备好的陈述 反而。使用下面概述的策略需要您自担风险。(还,
mysql_real_escape_string()
在 PHP 7 中被删除。)
如果您使用的是最新版本的 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
.
另外,正如其他人所建议的,您可能会发现使用类似的东西来提升抽象层很有用/更容易 磷酸二氢钾.
请注意,您询问的案例相当简单,更复杂的案例可能需要更复杂的方法。尤其:
这里的每个答案都只涵盖了问题的一部分。事实上,有 四 我们可以动态添加不同的查询部分:-
- 一个字符串
- 一个号码
- 一个标识符
- 语法关键字。
准备好的陈述仅涵盖其中两个。
但有时我们必须使查询更加动态,同时添加运算符或标识符。因此,我们需要不同的保护技术。
一般来说,这种保护方法是基于 白名单.
在这种情况下,每个动态参数都应该在脚本中进行硬编码,并从该集中进行选择。例如,要进行动态排序:
$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 注入的措施(如果适用)。
如果您仍然不相信,这是我写的逐步解释, 预防 SQL 注入的搭便车指南, ,我详细解释了所有这些问题,甚至编写了一个专门讨论不良做法及其披露的部分。
我建议使用 磷酸二氢钾 (PHP 数据对象)运行参数化 SQL 查询。
这不仅可以防止 SQL 注入,还可以加快查询速度。
通过使用 PDO 而不是 mysql_
, mysqli_
, , 和 pgsql_
函数,您可以使您的应用程序更加从数据库中抽象出来,在极少数情况下您必须切换数据库提供程序。
使用 PDO
并准备好询问。
($conn
是一个 PDO
目的)
$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();
正如您所看到的,人们建议您最多使用准备好的语句。这没有错,但是当你的查询执行时 就一次 每个进程都会有轻微的性能损失。
我遇到了这个问题,但我想我已经解决了 非常 复杂的方式 - 黑客用来避免使用引号的方式。我将其与模拟的准备好的语句结合使用。我用它来防止 全部 各种可能的 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')
十六进制是完美的逃脱方式。没有办法注射。
UNHEX函数和0x前缀的区别
评论里有一些讨论,所以我终于想澄清一下。这两种方法非常相似,但在某些方面略有不同:
** 0x** 前缀只能用于数据列,例如 char、varchar、文本、块、二进制等.
另外,如果您要插入空字符串,它的使用会有点复杂。你必须将其完全替换为 ''
, ,否则你会得到一个错误。
十六进制() 致力于 任何 柱子;您不必担心空字符串。
十六进制方法经常被用作攻击
请注意,这种十六进制方法通常用作 SQL 注入攻击,其中整数就像字符串一样,只需使用 mysql_real_escape_string
. 。那么你就可以避免使用引号。
例如,如果你只是做这样的事情:
"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])
一次攻击可以给你注入非常 容易地. 。考虑从脚本返回的以下注入代码:
选择 ...哪里 ID = -1 union all 从 information_schema.tables 中选择 table_name
现在只需提取表结构:
选择 ...WHERE id = -1 union all select column_name from information_schema.column where table_name = 0x61727469636c65
然后只需选择想要的任何数据即可。是不是很酷?
但是,如果可注入站点的编码器对其进行十六进制处理,则不可能进行注入,因为查询将如下所示: SELECT ... WHERE id = UNHEX('2d312075...3635')
已弃用警告:这个答案的示例代码(就像问题的示例代码一样)使用 PHP
MySQL
扩展,该扩展在 PHP 5.5.0 中已弃用,并在 PHP 7.0.0 中完全删除。安全警告:这个答案不符合安全最佳实践。 转义不足以防止 SQL 注入, , 使用 准备好的陈述 反而。使用下面概述的策略需要您自担风险。(还,
mysql_real_escape_string()
在 PHP 7 中被删除。)重要的
防止 SQL 注入的最佳方法是使用 准备好的报表 而不是逃避, , 作为 接受的答案 演示。
有一些图书馆,例如 光环数据库 和 易数据库 使开发人员能够更轻松地使用准备好的语句。详细了解为什么准备好的语句更擅长 停止SQL注入, ,参考 这
mysql_real_escape_string()
旁路 和 最近修复了 WordPress 中的 Unicode SQL 注入漏洞.
注射预防 - 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 注入预防.
已弃用警告:这个答案的示例代码(就像问题的示例代码一样)使用 PHP
MySQL
扩展,该扩展在 PHP 5.5.0 中已弃用,并在 PHP 7.0.0 中完全删除。安全警告:这个答案不符合安全最佳实践。 转义不足以防止 SQL 注入, , 使用 准备好的陈述 反而。使用下面概述的策略需要您自担风险。(还,
mysql_real_escape_string()
在 PHP 7 中被删除。)
你可以做一些基本的事情,如下所示:
$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query("INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
这并不能解决所有问题,但它是一个非常好的垫脚石。我遗漏了一些明显的项目,例如检查变量的存在、格式(数字、字母等)。
无论您最终使用什么,请确保检查您的输入尚未被破坏 magic_quotes
或其他一些善意的垃圾,如果有必要,将其贯穿 stripslashes
或任何对其进行消毒的东西。
已弃用警告:这个答案的示例代码(就像问题的示例代码一样)使用 PHP
MySQL
扩展,该扩展在 PHP 5.5.0 中已弃用,并在 PHP 7.0.0 中完全删除。安全警告:这个答案不符合安全最佳实践。 转义不足以防止 SQL 注入, , 使用 准备好的陈述 反而。使用下面概述的策略需要您自担风险。(还,
mysql_real_escape_string()
在 PHP 7 中被删除。)
参数化查询和输入验证是正确的选择。有很多场景可能会发生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 注入(当转义还不够时)
我赞成 存储过程 (MySQL从5.0开始就支持存储过程)从安全角度来看 - 优点是 -
- 大多数数据库(包括 MySQL) 使用户访问权限仅限于执行存储过程。细粒度的安全访问控制有助于防止权限升级攻击。这可以防止受感染的应用程序直接针对数据库运行 SQL。
- 它们从应用程序中抽象出原始 SQL 查询,因此应用程序可用的数据库结构信息较少。这使得人们更难理解数据库的底层结构并设计合适的攻击。
- 它们只接受参数,因此参数化查询的优点就在那里。当然 - IMO 你仍然需要清理你的输入 - 特别是如果你在存储过程中使用动态 SQL。
缺点是——
- 它们(存储过程)很难维护并且往往会快速繁殖。这使得管理它们成为一个问题。
- 它们不太适合动态查询 - 如果它们被构建为接受动态代码作为参数,那么很多优点就会被抵消。
有很多方法可以防止 SQL 注入和其他 SQL 攻击。您可以在互联网(Google 搜索)上轻松找到它。当然 PDO 是很好的解决方案之一。 但我想向您推荐一些防止 SQL 注入的好链接。
还有其他一些像 使用 MySQL 和 PHP 防止 SQL 注入
现在, 为什么需要防止查询受到 SQL 注入?
我想让你知道:为什么我们要尝试防止 SQL 注入,下面是一个简短的示例:
查询登录认证匹配:
$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";
现在,如果有人(黑客)把
$_POST['email']= admin@emali.com' OR '1=1
和密码任何东西......
该查询将被解析到系统中,最多可达:
$query="select * from users where email='admin@emali.com' OR '1=1';
另一部分将被丢弃。那么,会发生什么呢?未经授权的用户(黑客)无需密码即可以管理员身份登录。现在,他可以做管理员/电子邮件人员可以做的任何事情。看吧,如果不阻止 SQL 注入,那是非常危险的。
我想如果有人想使用 PHP 和 MySQL 或其他数据库服务器:
- 想想学习 磷酸二氢钾 (PHP 数据对象)——它是一个数据库访问层,提供访问多个数据库的统一方法。
- 想想学习 MySQLi
- 使用本机 PHP 函数,例如: 条带标签, mysql_real_escape_string 或者如果是变量数字,则
(int)$foo
. 。阅读有关 PHP 中变量类型的更多信息 这里. 。如果您使用 PDO 或 MySQLi 等库,请始终使用 PDO::引用() 和 mysqli_real_escape_string().
库示例:
---- 磷酸二氢钾
----- 没有占位符 - SQL 注入已经成熟! 这不好
$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values ($name, $addr, $city)");
----- 未命名占位符
$request = $pdoConnection->("INSERT INTO parents (name, addr, city) values (?, ?, ?);
----- 命名占位符
$request = $pdoConnection->("INSERT INTO parents (name, addr, city) value (:name, :addr, :city)");
--- MySQLi
$request = $mysqliConnection->prepare('
SELECT * FROM trainers
WHERE name = ?
AND email = ?
AND last_login > ?');
$query->bind_param('first_param', 'second_param', $mail, time() - 3600);
$query->execute();
附言:
PDO轻松赢得这场战斗。为了支持十二个不同的数据库驱动程序和命名参数,我们可以忽略小型性能损失,并适用于其API。从安全角度来看,只要开发人员以应使用的方式使用它们,它们都是安全的
但是,尽管PDO和MySQLI都非常快,但Mysqli在基准测试中的表现速度不大 - 无准备的陈述〜2.5%,而准备好的陈述则〜6.5%。
请测试对数据库的每个查询 - 这是防止注入的更好方法。
如果可能,请强制转换参数的类型。但它只适用于简单类型,如 int、bool 和 float。
$unsafe_variable = $_POST['user_id'];
$safe_variable = (int)$unsafe_variable ;
mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
如果您想利用缓存引擎,例如 雷迪斯 或者 内存缓存, ,也许 DALMP 可能是一个选择。它采用纯 MySQLi. 。检查一下: 使用 PHP 的 MySQL 的 DALMP 数据库抽象层。
此外,您可以在准备查询之前“准备”参数,以便可以构建动态查询并最终获得完全准备好的语句查询。 使用 PHP 的 MySQL 的 DALMP 数据库抽象层。
对于那些不确定如何使用 PDO(来自 mysql_
函数),我做了一个 非常非常简单的 PDO 包装器 那是一个文件。它的存在是为了展示完成应用程序需要完成的所有常见事情是多么容易。适用于 PostgreSQL、MySQL 和 SQLite。
基本上,读一下 当您阅读手册时 了解如何在现实生活中使用 PDO 函数,以便轻松存储和检索格式中的值 你 想。
我想要一个单列
$count = DB::column('SELECT COUNT(*) FROM `user`);
我想要一个数组(键=>值)结果(即用于制作选择框)
$pairs = DB::pairs('SELECT `id`, `username` FROM `user`);
我想要单行结果
$user = DB::row('SELECT * FROM `user` WHERE `id` = ?', array($user_id));
我想要一系列结果
$banned_users = DB::fetch('SELECT * FROM `user` WHERE `banned` = ?', array(TRUE));
使用这个 PHP 函数 mysql_escape_string()
您可以快速得到良好的预防。
例如:
SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."'
mysql_escape_string
— 转义字符串以在 mysql_query 中使用
为了更多预防,您可以在末尾添加...
wHERE 1=1 or LIMIT 1
最后你得到:
SELECT * FROM users WHERE name = '".mysql_escape_string($name_from_html_form)."' LIMIT 1
在 SQL 语句中转义特殊字符的一些准则。
不要使用 MySQL, ,此扩展已弃用,请使用 MySQLi 或者 磷酸二氢钾.
MySQLi
要手动转义字符串中的特殊字符,您可以使用 mysqli_real_escape_string 功能。除非设置了正确的字符集,否则该功能将无法正常工作 mysqli_set_charset.
例子:
$mysqli = new mysqli( 'host', 'user', 'password', 'database' );
$mysqli->set_charset( 'charset');
$string = $mysqli->real_escape_string( $string );
$mysqli->query( "INSERT INTO table (column) VALUES ('$string')" );
要使用准备好的语句自动转义值,请使用 mysqli_prepare, , 和 mysqli_stmt_bind_param 其中必须提供相应绑定变量的类型才能进行适当的转换:
例子:
$stmt = $mysqli->prepare( "INSERT INTO table ( column1, column2 ) VALUES (?,?)" );
$stmt->bind_param( "is", $integer, $string );
$stmt->execute();
无论您使用预准备语句还是 mysqli_real_escape_string,您始终必须知道正在使用的输入数据的类型。
因此,如果您使用准备好的语句,则必须为 mysqli_stmt_bind_param 函数指定变量的类型。
顾名思义,使用 mysqli_real_escape_string 是为了转义字符串中的特殊字符,因此它不会使整数安全。该函数的目的是防止 SQL 语句中的字符串被破坏,以及由此可能导致的数据库损坏。如果使用得当,尤其是与 sprintf 结合使用时,mysqli_real_escape_string 是一个有用的函数。
例子:
$string = "x' OR name LIKE '%John%";
$integer = '5 OR id != 0';
$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string( $string ), $integer );
echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 5
$integer = '99999999999999999999';
$query = sprintf( "SELECT id, email, pass, name FROM members WHERE email ='%s' AND id = %d", $mysqli->real_escape_string( $string ), $integer );
echo $query;
// SELECT id, email, pass, name FROM members WHERE email ='x\' OR name LIKE \'%John%' AND id = 2147483647
这个问题的简单替代方案可以通过在数据库本身中授予适当的权限来解决。例如:如果您使用的是 MySQL 数据库,则通过终端或提供的 UI 进入数据库,然后按照以下命令操作:
GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';
这将限制用户只能使用指定的查询。删除删除权限,这样数据就永远不会从 PHP 页面触发的查询中删除。第二件事是刷新权限,以便MySQL刷新权限并更新。
FLUSH PRIVILEGES;
有关的更多信息 冲水.
要查看用户的当前权限,请触发以下查询。
select * from mysql.user where User='username';
学习更多关于 授予.
安全警告:这个答案不符合安全最佳实践。 转义不足以防止 SQL 注入, , 使用 准备好的陈述 反而。使用下面概述的策略需要您自担风险。(还,
mysql_real_escape_string()
在 PHP 7 中被删除。)已弃用警告:mysql 扩展目前已被弃用。我们建议使用 PDO扩展
我使用三种不同的方法来防止我的 Web 应用程序容易受到 SQL 注入的攻击。
- 用于
mysql_real_escape_string()
, ,这是一个预定义的函数 PHP, ,此代码向以下字符添加反斜杠:\x00
,\n
,\r
,\
,'
,"
和\x1a
. 。将输入值作为参数传递,以最大限度地减少 SQL 注入的机会。 - 最先进的方法是使用 PDO。
我希望这能帮到您。
考虑以下查询:
$iId = mysql_real_escape_string("1 OR 1=1");
$sSql = "SELECT * FROM table WHERE id = $iId";
mysql_real_escape_string() 不会在这里进行保护。如果您在查询中的变量周围使用单引号 (' '),就可以防止出现这种情况。下面是一个解决方案:
$iId = (int) mysql_real_escape_string("1 OR 1=1");
$sSql = "SELECT * FROM table WHERE id = $iId";
这 问题 对此有一些很好的答案。
我建议,使用 PDO 是最好的选择。
编辑:
mysql_real_escape_string()
自 PHP 5.5.0 起已弃用。使用 mysqli 或 PDO。
mysql_real_escape_string() 的替代方法是
string mysqli_real_escape_string ( mysqli $link , string $escapestr )
例子:
$iId = $mysqli->real_escape_string("1 OR 1=1");
$mysqli->query("SELECT * FROM table WHERE id = $iId");
关于许多有用的答案,我希望能为这个帖子添加一些价值。SQL注入是一种可以通过用户输入(由用户填充然后在查询中使用的输入)来完成的攻击,SQL注入模式是正确的查询语法,我们可以将其称为:由于不良原因导致不良查询,我们假设可能有坏人试图获取影响安全三项原则(机密性、完整性、可用性)的秘密信息(绕过访问控制)。
现在,我们的重点是防止 SQL 注入攻击等安全威胁,问题是(如何防止使用 PHP 进行 SQL 注入攻击),更现实一点,数据过滤或清除输入数据是在这样的内部使用用户输入数据时的情况查询,使用 PHP 或任何其他编程语言不是这样,或者正如更多人建议使用现代技术,例如准备好的语句或当前支持 SQL 注入预防的任何其他工具,认为这些工具不再可用了吗?您如何保护您的应用程序?
我针对 SQL 注入的方法是:在将用户输入数据发送到数据库之前(在任何查询中使用它之前)清除用户输入数据。
数据过滤(将不安全数据转换为安全数据)考虑一下 磷酸二氢钾 和 MySQLi 不可用,如何保护您的应用程序?你强迫我使用它们吗?那么除了 PHP 之外的其他语言呢?我更喜欢提供一般性的想法,因为它可以用于更广泛的边界,而不仅仅是特定的语言。
- SQL用户(限制用户权限):最常见的 SQL 操作是(SELECT、UPDATE、INSERT),那么,为什么要给不需要 UPDATE 权限的用户呢?例如 登录和搜索页面 都只使用了SELECT,那么,为什么在这些具有高权限的页面中使用DB用户呢?规则:不要为所有权限创建一个数据库用户,对于所有 SQL 操作,您可以创建像 (deluser, selectuser, updateuser) 这样的方案作为用户名以方便使用。
看 最小特权原则
数据过滤:在构建任何查询之前,应该验证和过滤用户输入,对于程序员来说,为每个用户输入变量定义一些属性非常重要:数据类型、数据模式和数据长度. 。对于 (x 和 y) 之间的数字字段,必须使用精确规则进行精确验证,对于字符串(文本)字段:模式就是这种情况,例如,用户名必须仅包含一些字符,例如 [a-zA-Z0-9_-.] 长度在 (x 和 n) 之间变化,其中 x 和 n (整数, x <=n )。规则:创建精确的过滤器和验证规则对我来说是最佳实践。
使用其他工具:在这里,我也同意你的观点,即准备好的语句(参数化查询)和存储过程,这里的缺点是这些方式需要高级技能,而这对于大多数用户来说是不存在的,这里的基本思想是区分SQL查询和数据即使在内部使用的数据中,这两种方法也可以使用不安全的数据,因为此处的用户输入数据不会向原始查询添加任何内容,例如(any 或 x=x)。欲了解更多信息,请阅读 OWASP SQL 注入预防备忘单.
现在,如果您是高级用户,可以根据需要开始使用这种防御,但是,对于初学者来说,如果他们无法快速实现存储过程并准备语句,那么最好尽可能地过滤输入数据。
最后,让我们考虑用户发送下面的文本而不是输入他的用户名:
[1] UNION SELECT IF(SUBSTRING(Password,1,1)='2',BENCHMARK(100000,SHA1(1)),0) User,Password FROM mysql.user WHERE User = 'root'
可以在没有任何准备好的语句和存储过程的情况下尽早检查此输入,但为了安全起见,在用户数据过滤和验证之后开始使用它们。
最后一点是检测意外行为,这需要更多的努力和复杂性;不建议将其用于普通 Web 应用程序。上述用户输入中的意外行为是 SELECT、UNION、IF、SUBSTRING、BENCHMARK、SHA、root 一旦检测到这些单词,您就可以避免输入。
更新1:
有网友评论说这个帖子没用,好吧!这是什么 OWASP.ORG 假如:
主要防御:
选项1:使用准备好的语句(参数化查询)
选项#2:存储过程的使用
选项#3:转义所有用户提供的输入
附加防御:
同时强制执行:最低权限
还执行:白名单输入验证
如您所知,声称一篇文章应该有有效的论据支持,至少有一个参考文献!否则,将被视为攻击和不良主张!
更新2:
从 PHP 手册来看, PHP:准备好的报表 - 手册:
转义和SQL注入
绑定变量将被服务器自动转义。这 服务器将其转义值插入到 执行前的语句模板。必须向 server 为绑定变量的类型,以创建适当的 转换。有关更多信息,请参阅 mysqli_stmt_bind_param() 函数 信息。
服务器内值的自动转义有时是 被认为是防止 SQL 注入的安全功能。一样 在以下情况下,可以使用未准备好的语句实现安全程度 输入值已正确转义。
更新3:
我创建了测试用例来了解 PDO 和 MySQLi 在使用准备好的语句时如何将查询发送到 MySQL 服务器:
原产地保护组织:
$user = "''1''"; //Malicious keyword
$sql = 'SELECT * FROM awa_user WHERE userame =:username';
$sth = $dbh->prepare($sql, array(PDO::ATTR_CURSOR => PDO::CURSOR_FWDONLY));
$sth->execute(array(':username' => $user));
查询日志:
189 Query SELECT * FROM awa_user WHERE userame ='\'\'1\'\'' 189 Quit
MySQLi:
$stmt = $mysqli->prepare("SELECT * FROM awa_user WHERE username =?")) {
$stmt->bind_param("s", $user);
$user = "''1''";
$stmt->execute();
查询日志:
188 Prepare SELECT * FROM awa_user WHERE username =? 188 Execute SELECT * FROM awa_user WHERE username ='\'\'1\'\'' 188 Quit
很明显,准备好的语句也在转义数据,仅此而已。
正如上面的声明中也提到的 The automatic escaping of values within the server is sometimes considered a security feature to prevent SQL injection. The same degree of security can be achieved with non-prepared statements, if input values are escaped correctly
, ,因此,这证明了数据验证,例如 intval()
在发送任何查询之前使用整数值是一个好主意,此外,在发送查询之前防止恶意用户数据是 正确有效的做法.
请参阅此问题了解更多详细信息: PDO 向 MySQL 发送原始查询,而 Mysqli 发送准备好的查询,两者产生相同的结果
参考:
** 警告:这个答案中描述的方法仅适用于非常特定的场景并且不安全,因为 SQL 注入攻击不仅仅依赖于能够注入 X=Y
.**
如果攻击者试图通过 PHP 侵入表单 $_GET
变量或 URL 的查询字符串,如果它们不安全,您将能够捕获它们。
RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php
因为 1=1
, 2=2
, 1=2
, 2=1
, 1+1=2
, , ETC...是攻击者对 SQL 数据库提出的常见问题。也许它也被许多黑客应用程序使用。
但您必须小心,不得从您的站点重写安全查询。上面的代码给你一个提示,重写或重定向 (这取决于你) 将特定于黑客攻击的动态查询字符串放入一个页面中,该页面将存储攻击者的 IP地址, ,甚至是他们的 COOKIES、历史记录、浏览器或任何其他敏感信息,因此您可以稍后通过禁止他们的帐户或联系当局来处理它们。
有很多答案 PHP 和 MySQL, ,但这里是代码 PHP 和 Oracle 用于防止 SQL 注入以及定期使用 oci8 驱动程序:
$conn = oci_connect($username, $password, $connection_string);
$stmt = oci_parse($conn, 'UPDATE table SET field = :xx WHERE ID = 123');
oci_bind_by_name($stmt, ':xx', $fieldval);
oci_execute($stmt);
一个好主意是使用 “对象关系映射器” 喜欢 成语:
$user = ORM::for_table('user')
->where_equal('username', 'j4mie')
->find_one();
$user->first_name = 'Jamie';
$user->save();
$tweets = ORM::for_table('tweet')
->select('tweet.*')
->join('user', array(
'user.id', '=', 'tweet.user_id'
))
->where_equal('user.username', 'j4mie')
->find_many();
foreach ($tweets as $tweet) {
echo $tweet->text;
}
它不仅可以帮助您避免 SQL 注入,还可以避免语法错误!还支持具有方法链接的模型集合,以一次过滤或将操作应用于多个结果和多个连接。
已弃用警告:这个答案的示例代码(就像问题的示例代码一样)使用 PHP
MySQL
扩展,该扩展在 PHP 5.5.0 中已弃用,并在 PHP 7.0.0 中完全删除。安全警告:这个答案不符合安全最佳实践。 转义不足以防止 SQL 注入, , 使用 准备好的陈述 反而。使用下面概述的策略需要您自担风险。(还,
mysql_real_escape_string()
在 PHP 7 中被删除。)
使用 磷酸二氢钾 和 MYSQL 是防止 SQL 注入的一个好习惯,但是如果你真的想使用 MySQL 函数和查询,最好使用
$unsafe_variable = mysql_real_escape_string($_POST['user_input']);
还有更多的能力可以防止这种情况发生:就像识别一样——如果输入是字符串、数字、字符或数组,有很多内置函数可以检测到这一点。另外,最好使用这些函数来检查输入数据。
$unsafe_variable = (is_string($_POST['user_input']) ? $_POST['user_input'] : '');
$unsafe_variable = (is_numeric($_POST['user_input']) ? $_POST['user_input'] : '');
使用这些函数来检查输入数据要好得多 mysql_real_escape_string
.
几年前我写过这个小函数:
function sqlvprintf($query, $args)
{
global $DB_LINK;
$ctr = 0;
ensureConnection(); // Connect to database if not connected already.
$values = array();
foreach ($args as $value)
{
if (is_string($value))
{
$value = "'" . mysqli_real_escape_string($DB_LINK, $value) . "'";
}
else if (is_null($value))
{
$value = 'NULL';
}
else if (!is_int($value) && !is_float($value))
{
die('Only numeric, string, array and NULL arguments allowed in a query. Argument '.($ctr+1).' is not a basic type, it\'s type is '. gettype($value). '.');
}
$values[] = $value;
$ctr++;
}
$query = preg_replace_callback(
'/{(\\d+)}/',
function($match) use ($values)
{
if (isset($values[$match[1]]))
{
return $values[$match[1]];
}
else
{
return $match[0];
}
},
$query
);
return $query;
}
function runEscapedQuery($preparedQuery /*, ...*/)
{
$params = array_slice(func_get_args(), 1);
$results = runQuery(sqlvprintf($preparedQuery, $params)); // Run query and fetch results.
return $results;
}
这允许以单行 C# 风格的 String.Format 运行语句,例如:
runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);
考虑到变量类型,它会逃脱。如果您尝试参数化表名、列名,它将失败,因为它将每个字符串放在引号中,这是无效的语法。
安全更新:以前的 str_replace
该版本允许通过将 {#} 令牌添加到用户数据中来进行注入。这 preg_replace_callback
如果替换包含这些标记,则版本不会引起问题。