提问人: 提问时间:9/13/2008 最后编辑:44 revs, 36 users 14%Andrew G. Johnson 更新时间:7/17/2023 访问量:2163301
如何防止PHP中的SQL注入?
How can I prevent SQL injection in PHP?
问:
如果在未修改的情况下将用户输入插入到 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;--')
可以做些什么来防止这种情况发生?
答:
我建议使用 PDO(PHP 数据对象)来运行参数化 SQL 查询。
这不仅可以防止 SQL 注入,还可以加快查询速度。
通过使用 PDO 而不是 、 和 函数,可以使应用程序从数据库中更加抽象,这在极少数情况下必须切换数据库提供程序。mysql_
mysqli_
pgsql_
评论
substitute every variable in your query with a placeholder
安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用预准备语句。使用下面概述的策略,风险自负。
你可以做一些基本的事情,比如:
$safe_variable = mysqli_real_escape_string($dbConnection, $_POST["user-input"]);
mysqli_query($dbConnection, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
这并不能解决所有问题,但它是一个很好的垫脚石。我省略了明显的项目,例如检查变量的存在、格式(数字、字母等)。
评论
$q = "SELECT col FROM tbl WHERE x = $safe_var";
$safe_var
1 UNION SELECT password FROM users
CONCAT
CHR
mysql_real_escape_string
现已弃用,因此它不再是一个可行的选择。它将来会从 PHP 中删除。最好转到PHP或MySQL人员推荐的内容。
若要使用参数化查询,需要使用 Mysqli 或 PDO。要使用 mysqli 重写您的示例,我们需要如下所示的内容。
<?php
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli("server", "username", "password", "database_name");
$variable = $_POST["user-input"];
$stmt = $mysqli->prepare("INSERT INTO table (column) VALUES (?)");
// "s" means the database expects a string
$stmt->bind_param("s", $variable);
$stmt->execute();
你要读的关键函数是mysqli::p repare
。
此外,正如其他人所建议的那样,您可能会发现使用 PDO 之类的东西来加强抽象层是有用/更容易的。
请注意,您询问的案例相当简单,更复杂的案例可能需要更复杂的方法。特别:
- 如果要根据用户输入更改 SQL 的结构,则参数化查询将无济于事,并且 不涵盖所需的转义。在这种情况下,最好将用户的输入通过白名单传递,以确保只允许“安全”值通过。
mysql_real_escape_string
评论
mysql_real_escape_string
htmlentities
mysql_real_escape_string()
mysql_*
mysqli_*
mysqli_real_escape_string
避免SQL注入攻击的正确方法,无论你使用哪个数据库,都是将数据与SQL分离,这样数据就保留在数据中,永远不会被SQL解析器解释为命令。可以使用格式正确的数据部分创建 SQL 语句,但如果您不完全了解详细信息,则应始终使用准备好的语句和参数化查询。这些是与任何参数分开发送到数据库服务器并由数据库服务器解析的 SQL 语句。这样,攻击者就不可能注入恶意 SQL。
基本上有两种选择来实现这一点:
使用 PDO(对于任何受支持的数据库驱动程序):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute([ 'name' => $name ]); foreach ($stmt as $row) { // Do something with $row }
使用 MySQLi(用于 MySQL):
从 PHP 8.2+ 开始,我们可以利用execute_query()
在一种方法中准备、绑定参数和执行 SQL 语句:$result = $db->execute_query('SELECT * FROM employees WHERE name = ?', [$name]); while ($row = $result->fetch_assoc()) { // Do something with $row }
最高PHP8.1:
$stmt = $db->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 以外的数据库,则可以参考特定于驱动程序的第二个选项(例如,对于 PostgreSQL)。PDO是通用选项。pg_prepare()
pg_execute()
正确设置连接
PDO的
请注意,使用 PDO 访问 MySQL 数据库时,默认情况下不使用真正的预准备语句。要解决此问题,您必须禁用预准备语句的模拟。使用 PDO 创建连接的示例如下:
$dbConnection = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8mb4', 'user', 'password');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
在上面的示例中,错误模式并不是绝对必要的,但建议添加它。这样,PDO将通过抛出.PDOException
但是,必须的是第一行,它告诉 PDO 禁用模拟的预准备语句并使用真正的预准备语句。这确保了在将语句和值发送到MySQL服务器之前,PHP不会对其进行解析(使可能的攻击者没有机会注入恶意SQL)。setAttribute()
虽然您可以在构造函数的选项中设置 ,但需要注意的是,“旧”版本的 PHP(5.3.6 之前)会静默地忽略 DSN 中的 charset 参数。charset
Mysqli的
对于mysqli,我们必须遵循相同的例程:
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); // error reporting
$dbConnection = new mysqli('127.0.0.1', 'username', 'password', 'test');
$dbConnection->set_charset('utf8mb4'); // charset
解释
传递到的 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([ 'column' => $unsafeValue ]);
预准备语句可以用于动态查询吗?
虽然您仍然可以对查询参数使用预准备语句,但动态查询本身的结构无法参数化,某些查询功能也无法参数化。
对于这些特定方案,最好的办法是使用限制可能值的白名单筛选器。
// Value whitelist
// $dir can only be 'DESC', otherwise it will be 'ASC'
if (empty($dir) || $dir !== 'DESC') {
$dir = 'ASC';
}
评论
使用和准备的查询。PDO
($conn
是一个对象)PDO
$stmt = $conn->prepare("INSERT INTO tbl VALUES(:id, :name)");
$stmt->bindValue(':id', $id);
$stmt->bindValue(':name', $name);
$stmt->execute();
无论你最终使用什么,确保你检查你的输入没有被其他善意的垃圾破坏,如果有必要,运行它或任何东西来消毒它。magic_quotes
stripslashes
评论
从安全的角度来看,我更喜欢存储过程(MySQL 从 5.0 开始就支持存储过程)——优点是 -
- 大多数数据库(包括 MySQL)允许用户访问限制为执行存储过程。细粒度的安全访问控制有助于防止权限升级攻击。这样可以防止被入侵的应用程序直接对数据库运行 SQL。
- 它们从应用程序中抽象出原始 SQL 查询,因此应用程序可用的数据库结构信息较少。这使得人们更难理解数据库的底层结构并设计合适的攻击。
- 它们只接受参数,因此参数化查询的优点是存在的。当然 - IMO 你仍然需要清理你的输入 - 特别是当你在存储过程中使用动态 SQL。
缺点是 -
- 它们(存储过程)很难维护,并且往往会很快成倍增加。这使得管理它们成为一个问题。
- 它们不太适合动态查询 - 如果它们被构建为接受动态代码作为参数,那么很多优点就会被否定。
已弃用的警告:这个答案的示例代码(就像问题的示例代码一样)使用了 PHP 的扩展,该扩展在 PHP 5.5.0 中被弃用,并在 PHP 7.0.0 中被完全删除。
MySQL
安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用预准备语句。使用下面概述的策略,风险自负。(此外,在 PHP 7 中被删除。
mysql_real_escape_string()
重要
防止 SQL 注入的最佳方法是使用预准备语句而不是转义,正如公认的答案所示。
有一些库,如 Aura.Sql 和 EasyDB,允许开发人员更轻松地使用准备好的语句。要详细了解为什么预准备语句更擅长停止 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 注入防护中找到更多详细信息。
评论
mysql_real_escape_string
mysql_real_escape_string
现已弃用,因此它不再是一个可行的选择。它将来会从 PHP 中删除。最好转到PHP或MySQL人员推荐的内容。
已弃用的警告:这个答案的示例代码(就像问题的示例代码一样)使用了 PHP 的扩展,该扩展在 PHP 5.5.0 中被弃用,并在 PHP 7.0.0 中被完全删除。
MySQL
安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用预准备语句。使用下面概述的策略,风险自负。(此外,在 PHP 7 中被删除。
mysql_real_escape_string()
参数化查询和输入验证是要走的路。在许多情况下,即使已经使用了 SQL 注入,也可能发生 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 注入(当转义不够时)
评论
这里的每个答案都只涵盖了问题的一部分。 事实上,我们可以动态地将四个不同的查询部分添加到SQL中:
- 字符串
- 一个数字
- 标识符
- 语法关键字
准备好的陈述只涵盖其中的两个。
但有时我们必须使我们的查询更加动态,同时添加运算符或标识符。 因此,我们将需要不同的保护技术。
通常,这种保护方法基于白名单。
在这种情况下,每个动态参数都应该在脚本中硬编码,并从该集中选择。 例如,要执行动态排序:
$orders = array("name", "price", "qty"); // Field names
$key = array_search($_GET['sort'], $orders)); // if we have such a name
$orderby = $orders[$key]; // If not, first one will be set automatically.
$query = "SELECT * FROM `table` ORDER BY $orderby"; // Value is safe
为了简化这个过程,我编写了一个白名单辅助函数,该函数在一行中完成所有工作:
$orderby = white_list($_GET['orderby'], "name", ["name","price","qty"], "Invalid field name");
$query = "SELECT * FROM `table` ORDER BY `$orderby`"; // sound and safe
还有另一种方法可以保护标识符 - 转义,但我更愿意坚持将白名单作为一种更强大、更明确的方法。然而,只要你有一个引用的标识符,你就可以对引用字符进行转义,以确保它的安全。例如,默认情况下,对于 mysql,您必须将引号字符加倍才能对其进行转义。对于其他 DBMS,转义规则会有所不同。
尽管如此,SQL语法关键字(如,等)仍然存在问题,但在这种情况下,白名单似乎是唯一的方法。AND
DESC
因此,一般性建议可以表述为
- 任何表示 SQL 数据文本(或者,简单地说 - SQL 字符串或数字)的变量都必须通过预准备语句添加。没有例外。
- 任何其他查询部分(如 SQL 关键字、表或字段名称或运算符)都必须通过白名单进行筛选。
更新
尽管对 SQL 注入保护的最佳实践达成了普遍共识,但仍然存在许多不良做法。其中一些已经深深地扎根于PHP用户的脑海中。例如,在这个页面上,有 80 多个已删除的答案(尽管大多数访问者看不到)——由于质量差或宣传不良和过时的做法,所有答案都被社区删除了。更糟糕的是,一些糟糕的答案并没有被删除,而是繁荣昌盛。
例如,有(1)个(2)仍然(3)个许多(4)个答案 (5),包括第二大赞成的答案,建议你手动字符串转义 - 一种过时的方法,被证明是不安全的。
或者有一个稍微好一点的答案,它只是建议另一种字符串格式化方法,甚至吹嘘它是终极灵丹妙药。当然,事实并非如此。这种方法并不比常规字符串格式好,但它保留了所有缺点:它仅适用于字符串,并且与任何其他手动格式一样,它本质上是可选的、非强制性的措施,容易出现任何类型的人为错误。
我认为这一切都是因为一个非常古老的迷信,得到了OWASP或PHP手册等权威的支持,它宣称任何“逃逸”和保护免受SQL注入之间的平等。
不管PHP手册怎么说,*_escape_string
绝不能使数据安全,也从来都不是故意的。除了对字符串以外的任何 SQL 部分都无用之外,手动转义是错误的,因为它是手动的,与自动相反。
而OWASP使情况变得更糟,强调逃避用户输入,这完全是无稽之谈:在注入保护的上下文中不应该有这样的词。每个变量都具有潜在的危险性 - 无论来源如何!或者,换句话说,每个变量都必须正确格式化才能放入查询中 - 无论来源如何。目的地才是最重要的。当开发人员开始将绵羊与山羊分开时(考虑某些特定变量是否“安全”),他/她就迈出了走向灾难的第一步。更不用说,即使是措辞也暗示在入口点进行批量转义,类似于非常神奇的引号功能——已经被鄙视、弃用和删除。
因此,与任何“转义”不同,预准备语句确实是防止 SQL 注入(如果适用)的措施。
评论
array_search()
可以返回一个整数(包括 ),或者 -- 应该重新审视这个答案的这一方面。0
false
如果可能,请强制转换参数的类型。但它仅适用于 int、bool 和 float 等简单类型。
$unsafe_variable = $_POST['user_id'];
$safe_variable = (int)$unsafe_variable ;
mysqli_query($conn, "INSERT INTO table (column) VALUES ('" . $safe_variable . "')");
评论
在我看来,通常防止 PHP 应用程序(或任何 Web 应用程序,就此而言)中注入 SQL 的最佳方法是考虑应用程序的架构。如果防止SQL注入的唯一方法是记住使用一种特殊的方法或函数,每次与数据库对话时都做正确的事情,那么你就做错了。这样一来,您忘记在代码中的某个时刻正确设置查询格式只是时间问题。
采用 MVC 模式和 CakePHP 或 CodeIgniter 等框架可能是正确的方法:创建安全数据库查询等常见任务已经在此类框架中得到解决并集中实现。它们可帮助您以合理的方式组织 Web 应用程序,并使您更多地考虑加载和保存对象,而不是安全地构建单个 SQL 查询。
评论
有许多方法可以防止 SQL 注入和其他 SQL 黑客攻击。您可以在互联网上轻松找到它(Google 搜索)。当然,PDO是很好的解决方案之一。但我想向你建议一些很好的链接预防SQL注入。
Microsoft 对 PHP 中 SQL 注入和预防的解释
还有一些其他的,比如用MySQL和PHP防止SQL注入。
现在,为什么需要阻止查询进行 SQL 注入?
我想让你知道:为什么我们试图通过下面的简短示例来阻止SQL注入:
查询登录身份验证匹配项:
$query="select * from users where email='".$_POST['email']."' and password='".$_POST['password']."' ";
现在,如果有人(黑客)把
$_POST['email']= [email protected]' OR '1=1
并输入任何密码......
查询将仅解析到系统中,最多:
$query="select * from users where email='[email protected]' OR '1=1';
另一部分将被丢弃。那么,会发生什么?未经授权的用户(黑客)将能够在没有密码的情况下以管理员身份登录。现在,他/她可以执行管理员/电子邮件人员可以执行的任何操作。看,如果不阻止SQL注入,这是非常危险的。
安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用预准备语句。使用下面概述的策略,风险自负。(此外,在 PHP 7 中被删除。
mysql_real_escape_string()
警告:此时删除了 mysql 扩展。我们建议使用 PDO 扩展
使用此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 注入,请改用预准备语句。使用下面概述的策略,风险自负。(此外,在 PHP 7 中被删除。
mysql_real_escape_string()
已弃用警告:mysql 扩展目前已弃用。我们建议使用 PDO 扩展
我使用三种不同的方法来防止我的 Web 应用程序容易受到 SQL 注入的攻击。
- 使用 ,这是 PHP 中的预定义函数,此代码将反斜杠添加到以下字符:、、、、和 。将输入值作为参数传递,以最大程度地减少 SQL 注入的可能性。
mysql_real_escape_string()
\x00
\n
\r
\
'
"
\x1a
- 最先进的方法是使用 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");
对于那些不确定如何使用PDO(来自函数)的人,我做了一个非常非常简单的PDO包装器,它是一个文件。它的存在是为了表明做应用程序需要完成的所有常见事情是多么容易。适用于 PostgreSQL、MySQL 和 SQLite。mysql_
基本上,在阅读手册的同时阅读它,了解如何将 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'));
正如你所看到的,人们建议你最多使用准备好的语句。这没有错,但是如果每个进程只执行一次查询,则性能会略有下降。
我面临着这个问题,但我认为我以非常复杂的方式解决了它——黑客用来避免使用引号的方式。我将其与模拟的预准备语句结合使用。我用它来防止各种可能的SQL注入攻击。
我的方法:
如果希望输入为整数,请确保它确实是整数。在像PHP这样的变量型语言中,这一点非常重要。例如,您可以使用这个非常简单但功能强大的解决方案:
sprintf("SELECT 1,2,3 FROM table WHERE 4 = %u", $input);
如果你期望从整数中得到任何其他东西,就十六进制它。如果你十六进制它,你将完美地转义所有输入。在 C/C++ 中有一个叫做
mysql_hex_string()
的函数,在 PHP 中你可以使用bin2hex()。
不要担心转义字符串会具有其原始长度的 2 倍大小,因为即使您使用 ,PHP 也必须分配相同的容量,这是相同的。
mysql_real_escape_string
((2*input_length)+1)
这种十六进制方法在传输二进制数据时经常使用,但我认为没有理由不对所有数据使用它来防止 SQL 注入攻击。请注意,您必须在数据前面加上或使用 MySQL 函数。
0x
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()
适用于任何列;您不必担心空字符串。
十六进制方法通常用作攻击
请注意,这种十六进制方法通常用作 SQL 注入攻击,其中整数就像字符串一样,并且仅使用 .然后,您可以避免使用引号。mysql_real_escape_string
例如,如果你只是做这样的事情:
"SELECT title FROM article WHERE id = " . mysql_real_escape_string($_GET["id"])
攻击可以很容易地注入你。请考虑从脚本返回的以下注入代码:
SELECT ... WHERE id = -1 UNION ALL SELECT table_name FROM information_schema.tables;
现在只需提取表结构:
SELECT ... WHERE id = -1 UNION ALL SELECT column_name FROM information_schema.column WHERE table_name = __0x61727469636c65__;
然后只需选择任何想要的数据。是不是很酷?
但是,如果可注入站点的编码人员对其进行十六进制,则不可能进行注入,因为查询将如下所示:
SELECT ... WHERE id = UNHEX('2d312075...3635');
评论
tblproducts
tblproducts
+
CONCAT
"'".$mysqli->escape_string($_GET["id"])."'"
我相信,如果有人打算将PHP与MySQL或任何其他数据库服务器一起使用:
附言:
PDO 具有支持 12 种不同的数据库驱动程序和命名参数等优点。从安全的角度来看,只要开发人员以应有的方式使用它们,它们都是安全的
评论
这个问题的简单替代方法可以通过在数据库本身中授予适当的权限来解决。 例如:如果您使用的是MySQL数据库,则通过终端或提供的UI进入数据库,然后按照以下命令操作:
GRANT SELECT, INSERT, DELETE ON database TO username@'localhost' IDENTIFIED BY 'password';
这将限制用户仅受指定查询的限制。删除删除权限,因此数据永远不会从从 PHP 页面触发的查询中删除。 第二件事是刷新权限,以便MySQL刷新权限和更新。
FLUSH PRIVILEGES;
有关 FLUSH 的更多信息。
若要查看用户的当前权限,请触发以下查询。
select * from mysql.user where User='username';
了解有关 GRANT 的更多信息。
评论
关于许多有用的答案,我希望为这个线程增加一些价值。
SQL注入是一种攻击,可以通过用户输入(由用户填充然后在查询中使用的输入)来完成。SQL注入模式是正确的查询语法,而我们可以称之为:出于不良原因的不良查询,我们假设可能有一个坏人试图获取影响安全性(机密性、完整性和可用性)三个原则的秘密信息(绕过访问控制)。
现在,我们的观点是防止诸如SQL注入攻击之类的安全威胁,问题问(如何使用PHP防止SQL注入攻击),更现实地说,数据过滤或清除输入数据是在此类查询中使用用户输入数据时的情况,使用PHP或任何其他编程语言的情况是不一样的,或者像更多人推荐的那样使用现代技术,例如准备语句或任何其他工具,目前支持SQL注入防护,认为这些工具已经不可用了吗?如何保护应用程序?
我反对SQL注入的方法是:在将用户输入数据发送到数据库之前(在任何查询中使用它之前)清除用户输入数据。
数据过滤(将不安全数据转换为安全数据)
考虑到 PDO 和 MySQLi 不可用。如何保护应用程序?你强迫我使用它们吗?除了PHP之外,其他语言呢?我更喜欢提供一般的想法,因为它可以用于更广泛的边界,而不仅仅是特定的语言。
- SQL 用户(限制用户权限):最常见的 SQL 操作是(SELECT、UPDATE、INSERT),那么,为什么要将 UPDATE 权限授予不需要它的用户呢?例如,登录和搜索页面只使用SELECT,那么,为什么在这些页面中使用具有高权限的DB用户呢?
规则:不要为所有权限创建一个数据库用户。对于所有 SQL 操作,您可以创建您的方案,例如 (deluser, selectuser, updateuser) 作为用户名,以便于使用。
请参阅最小特权原则。
数据筛选:在生成任何查询用户输入之前,应对其进行验证和筛选。对于程序员来说,为每个用户输入变量定义一些属性非常重要:数据类型、数据模式和数据长度。介于 (x 和 y) 之间的数字字段必须使用确切的规则进行精确验证,对于字符串(文本)字段:pattern 就是这种情况,例如,用户名必须仅包含一些字符,例如 [a-zA-Z0-9_-.]。长度在 (x 和 n) 之间变化,其中 x 和 n(整数,x <=n)。规则:创建精确的过滤器和验证规则对我来说是最佳实践。
使用其他工具:在这里,我也同意你的观点,即准备语句(参数化查询)和存储过程。这里的缺点是这些方法需要高级技能,而大多数用户并不存在这些技能。这里的基本思想是区分 SQL 查询和内部使用的数据。即使使用不安全的数据,也可以使用这两种方法,因为此处的用户输入数据不会向原始查询添加任何内容,例如 (any 或 x=x)。
有关更多信息,请阅读 OWASP SQL Injection Prevention 备忘单。
现在,如果您是高级用户,请随心所欲地开始使用此防御措施,但是,对于初学者来说,如果他们无法快速实现存储过程并准备语句,则最好尽可能多地筛选输入数据。
最后,让我们考虑用户在下面发送此文本,而不是输入他/她的用户名:
[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服务器:
PDO:
$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
很明显,准备好的语句也是在逃避数据,仅此而已。
如上所述,
服务器内值的自动转义有时被视为防止 SQL 注入的安全功能。如果正确转义输入值,则可以使用非预准备语句实现相同程度的安全性
因此,这证明在发送任何查询之前对整数值进行数据验证(例如)是一个好主意。此外,在发送查询之前防止恶意用户数据是一种正确有效的方法。intval()
有关更多详细信息,请参阅此问题:PDO向MySQL发送原始查询,而Mysqli发送准备好的查询,两者产生相同的结果
引用:
警告:此答案中描述的方法仅适用于非常具体的场景,并且不安全,因为 SQL 注入攻击不仅依赖于能够注入 X=Y
。
如果攻击者试图通过PHP的变量或使用URL的查询字符串入侵表单,那么如果它们不安全,您将能够捕获它们。$_GET
RewriteCond %{QUERY_STRING} ([0-9]+)=([0-9]+)
RewriteRule ^(.*) ^/track.php
因为 、 、 、 、 等......是攻击者的 SQL 数据库的常见问题。也许它也被许多黑客应用程序使用。1=1
2=2
1=2
2=1
1+1=2
但您必须小心,不得从您的网站重写安全查询。上面的代码为您提供了一个提示,将特定于黑客的动态查询字符串重写或重定向(取决于您)到一个页面中,该页面将存储攻击者的 IP 地址,甚至他们的 COOKIE、历史记录、浏览器或任何其他敏感信息,以便您以后可以通过禁止他们的帐户或联系当局来处理它们。
安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用预准备语句。
在 SQL 语句中转义特殊字符的一些准则。
不要使用 MySQL。此扩展已弃用。请改用 MySQLi 或 PDO。
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
顾名思义,使用 is 用于转义字符串中的特殊字符,因此它不会使整数安全。此函数的目的是防止破坏 SQL 语句中的字符串,以及它可能对数据库造成的损害。 使用得当时是一个有用的功能,尤其是与 结合使用时。mysqli_real_escape_string
mysqli_real_escape_string
sprintf
例:
$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
评论
一个简单的方法是使用像 CodeIgniter 或 Laravel 这样的 PHP 框架,它们具有过滤和活动记录等内置功能,这样您就不必担心这些细微差别。
评论
已弃用的警告:这个答案的示例代码(就像问题的示例代码一样)使用了 PHP 的扩展,该扩展在 PHP 5.5.0 中被弃用,并在 PHP 7.0.0 中被完全删除。
MySQL
安全警告:此答案不符合安全最佳实践。转义不足以防止 SQL 注入,请改用预准备语句。使用下面概述的策略,风险自负。(此外,在 PHP 7 中被删除。
mysql_real_escape_string()
使用 PDO 和 MYSQLi 是防止 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
评论
mysql_real_escape_string
现已弃用,因此它不再是一个可行的选择。它将来会从 PHP 中删除。最好转到PHP或MySQL人员推荐的内容。
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);
几年前我写过这个小函数:
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#-ish String.Format 中运行语句,例如:
runEscapedQuery("INSERT INTO Whatever (id, foo, bar) VALUES ({0}, {1}, {2})", $numericVar, $stringVar1, $stringVar2);
考虑到变量类型,它进行了转义。如果您尝试参数化表、列名,它将失败,因为它将每个字符串放在引号中,这是一种无效的语法。
安全更新:以前的版本允许通过将 {#} 个令牌添加到用户数据中来注入。如果替换包含这些令牌,则此版本不会导致问题。str_replace
preg_replace_callback
$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注入,还可以避免语法错误!它还支持具有方法链的模型集合,以一次筛选或将操作应用于多个结果和多个连接。
评论