如果将用户输入未经修改地插入到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。
您基本上有两种选择可以实现此目的:
-
使用PDO(对于任何受支持的数据库驱动程序):
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name'); $stmt->execute([ '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', 'password');
$dbConnection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$dbConnection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
在上面的示例中,错误模式不是严格必需的,但建议添加它。这样,Fatal Error
当出现问题时脚本不会以a停止。并且它为开发人员提供了解决catch
任何throw
n为PDOException
s的错误的机会。
但是,第一行是强制性的,setAttribute()
它告诉PDO禁用模拟的预处理语句并使用实际的预处理语句。这可以确保在将语句和值发送到MySQL服务器之前,不会对PHP进行语法分析(这使可能的攻击者没有机会注入恶意SQL)。
尽管可以charset
在构造函数的选项中设置,但是必须注意,PHP的“较旧”版本(在5.3.6之前)静默忽略了DSN中的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';
}
不建议使用的警告:
此答案的示例代码(如问题的示例代码)使用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
。
另外,正如其他人所建议的那样,您可能会发现有用/更容易地使用PDO之类的东西来增强抽象层。
请注意,您所询问的案例是一个相当简单的案例,更复杂的案例可能需要更复杂的方法。特别是:
- 如果要基于用户输入更改SQL的结构,则参数化查询将无济于事,并且不能满足要求的转义
mysql_real_escape_string
。在这种情况下,最好将用户的输入通过白名单,以确保仅允许“安全”值通过。 - 如果您在某种情况下使用用户输入中的整数并采用该
mysql_real_escape_string
方法,则将遭受以下多项式描述的多项式问题。这种情况比较棘手,因为整数不会被引号引起来,因此可以通过验证用户输入仅包含数字来进行处理。 - 可能还有其他我不知道的情况。您可能会发现这是一些有用的资源,可以解决您遇到的一些更细微的问题。
这里的每个答案仅涵盖部分问题。实际上,我们可以动态地将四个查询部分添加到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多个答案 -社区由于质量低劣或推广不良和过时的做法而将其删除。更糟糕的是,一些错误的答案并未被删除,反而会蒸蒸日上。
例如,There (1) 有(2) still(3) many(4)个 答案(5),其中第二个最重要的答案是建议您手动转义字符串-一种已被证明不安全的过时方法。
或者有一个更好的答案,它暗示了另一种字符串格式化方法,甚至称其为终极灵丹妙药。当然不是。这种方法并不比常规的字符串格式好,但是它保留了所有缺点:它仅适用于字符串,并且像其他任何手动格式一样,它本质上是可选的,非强制性的措施,容易出现任何类型的人为错误。
我认为所有这些都是由于一种非常古老的迷信,得到诸如OWASP或PHP手册之类的权威的支持,该权威宣称在“转义”和防止SQL注入之间应保持平等。
无论PHP手册使用了多长时间,都绝对不会*_escape_string
使数据安全,也永远不会使数据安全。除了对于字符串以外的任何SQL部分都没有用之外,手动转义是错误的,因为它是手动的,与自动的相反。
OWASP更加糟糕,它强调逃避用户输入,这完全是胡说八道:在注入保护的上下文中,不应有这样的措辞。每个变量都有潜在的危险-无论来源如何!换句话说,每个变量都必须正确设置格式才能放入查询中,而不管其来源如何。重要的是目的地。当开发人员开始将绵羊与山羊分开时(考虑某个特定变量是否“安全”),他/她迈出了走向灾难的第一步。更不用说即使是措辞也建议在入口点进行大量转义,类似于非常不可思议的引号功能-已被轻视,不推荐使用和删除。
因此,与无论“逃离”,准备好的语句是,确实从SQL注入保护措施(如适用)。
我建议使用PDO(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
,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');
不建议使用的警告:
此答案的示例代码(如问题的示例代码)使用PHP的MySQL
扩展名,该扩展名在PHP 5.5.0中已弃用,而在PHP 7.0.0中已完全删除。安全警告:此答案与安全最佳实践不符。转义不足以防止SQL注入,请改用准备好的语句。使用以下概述的策略需要您自担风险。(此外,
mysql_real_escape_string()
已在PHP 7中删除。)重要
如公认的答案所示,防止SQL注入的最佳方法是使用Prepared Statements 而不是escaping。
有一些库,例如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注入预防中找到更多详细信息。
您可以执行以下基本操作:
$safe_variable = mysqli_real_escape_string($_POST["user-input"], $dbConnection);
mysqli_query($dbConnection, "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中删除。)
参数化查询和输入验证是必经之路。即使mysql_real_escape_string()
使用了SQL注入,在很多情况下也可能发生。
这些示例容易受到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注入(当转义不够时)
我认为,通常防止在PHP应用程序(或任何Web应用程序)中进行SQL注入的最佳方法是考虑应用程序的体系结构。如果防止SQL注入的唯一方法是记住每次使用数据库时都使用一种执行“正确的事情”的特殊方法或函数,那么您做错了。这样,在代码中的某个时候忘记忘记正确设置查询格式就只是时间问题了。
采用MVC模式和诸如CakePHP或CodeIgniter之类的框架可能是正确的方法:诸如创建安全数据库查询之类的常见任务已在此类框架中得到解决和集中实现。它们可以帮助您以明智的方式组织Web应用程序,并使您更多地考虑加载和保存对象,而不是安全地构造单个SQL查询。
有很多方法可以防止SQL注入和其他SQL hack。您可以在Internet(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注入,那是非常危险的。
从安全的角度来看,我赞成存储过程(MySQL从5.0开始就支持存储过程)-优点是-
- 大多数数据库(包括MySQL)使用户访问仅限于执行存储过程。细粒度的安全访问控制对于防止特权攻击升级很有用。这样可以防止受感染的应用程序直接对数据库运行SQL。
- 他们从应用程序中提取原始SQL查询,因此应用程序可使用的数据库结构信息较少。这使人们更难理解数据库的底层结构并设计合适的攻击。
- 它们仅接受参数,因此存在参数化查询的优点。当然-IMO您仍然需要清理输入-特别是如果您在存储过程中使用动态SQL。
缺点是-
- 它们(存储过程)很难维护并且往往会快速繁殖。这使得管理它们成为一个问题。
- 它们不是非常适合动态查询-如果它们被构建为接受动态代码作为参数,那么许多优点就被否定了。
我认为如果有人想使用PHP和MySQL或其他一些数据库服务器:
- 考虑学习PDO(PHP数据对象)–它是数据库访问层,提供访问多个数据库的统一方法。
- 考虑学习MySQLi
- 使用本地PHP函数,例如:strip_tags,mysql_real_escape_string,或者如果是可变数字,则为just
(int)$foo
。在此处阅读有关PHP中变量类型的更多信息。如果您使用的是PDO或MySQLi之类的库,请始终使用PDO :: quote()和mysqli_real_escape_string()。
库示例:
---- PDO
-----没有占位符-可以进行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();
PS:
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 . "')");
如果您想利用Redis或Memcached之类的缓存引擎,也许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或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
函数变量的类型。
mysqli_real_escape_string
顾名思义,and的用途是用于转义字符串中的特殊字符,因此不会使整数安全。此功能的目的是防止破坏SQL语句中的字符串以及可能造成的数据库损坏。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
可以通过在数据库本身中授予适当的权限来解决此问题的简单替代方案。例如:如果您正在使用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';
了解有关GRANT的更多信息。
关于许多有用的答案,我希望为该线程增加一些价值。
SQL注入是一种攻击,可以通过用户输入(由用户填充然后在查询中使用的输入)来完成。SQL注入模式是正确的查询语法,我们可以称之为:错误查询是出于错误的原因,并且我们假设可能有一个错误的人试图获取影响安全性三原则(机密性)的秘密信息(绕过访问控制)。 ,完整性和可用性)。
现在,我们的观点是防止诸如SQL注入攻击之类的安全威胁,这个问题(如何使用PHP防止SQL注入攻击)更加现实,数据过滤或清除输入数据是在内部使用用户输入数据的情况这样的查询,使用PHP或任何其他编程语言的情况就不是这种情况,或者不是像更多人所推荐的那样使用现代技术(例如,准备好的语句或当前支持SQL注入预防的任何其他工具),是否认为这些工具不再可用?您如何保护您的应用程序?
我反对SQL注入的方法是:在将用户输入的数据发送到数据库之前(在任何查询中使用它之前),先清除它们。
数据过滤(将不安全数据转换为安全数据)
Consider that PDO and MySQLi are not available. How can you secure your application? Do you force me to use them? What about other languages other than PHP? I prefer to provide general ideas as it can be used for wider border, not just for a specific language.
- SQL user (limiting user privilege): most common SQL operations are (SELECT, UPDATE, INSERT), then, why give the UPDATE privilege to a user that does not require it? For example, login, and search pages are only using SELECT, then, why use DB users in these pages with high privileges?
RULE: do not create one database user for all privileges. For all SQL operations, you can create your scheme like (deluser, selectuser, updateuser) as usernames for easy usage.
请参阅最低特权原则。
-
数据过滤:在建立任何查询用户输入之前,应先对其进行验证和过滤。对于程序员而言,为每个用户输入变量定义一些属性很重要:
数据类型,数据模式和数据长度。(x和y)之间的数字的字段必须使用确切的规则进行严格验证,对于字符串(文本)的字段,则必须使用模式:pattern是这种情况,例如,用户名只能包含一些字符,让我们说[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:Prepared Statements-Manual:
转义和SQL注入
绑定的变量将由服务器自动转义。服务器在执行之前将其转义的值插入到语句模板的适当位置。必须向服务器提供有关绑定变量类型的提示,以创建适当的转换。有关更多信息,请参见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注入,请改用准备好的语句。使用以下概述的策略需要您自担风险。(此外,
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");
一种简单的方法是使用像CodeIgniter或Laravel这样的PHP框架,它们具有诸如过滤和活动记录的内置功能,因此您不必担心这些细微差别。
警告:此答案中描述的方法仅适用于非常特定的方案,并且并不安全,因为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
,等...都是攻击者的SQL数据库中的常见问题。也许它也被许多黑客应用程序所使用。
但是您必须小心,不要从站点重写安全的查询。上面的代码为您提供了一个技巧,可以将特定于黑客的动态查询字符串重写或重定向(取决于您)到页面中,该页面将存储攻击者的IP地址,甚至他们的菜谱,历史记录,浏览器或任何其他敏感信息信息,因此您以后可以通过禁止他们的帐户或与当局联系来与他们联系。
$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和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);
不建议使用的警告:
此答案的示例代码(如问题的示例代码)使用PHP的MySQL
扩展名,该扩展名在PHP 5.5.0中已弃用,而在PHP 7.0.0中已完全删除。安全警告:此答案与安全最佳实践不符。转义不足以防止SQL注入,请改用准备好的语句。使用以下概述的策略需要您自担风险。(此外,
mysql_real_escape_string()
已在PHP 7中删除。)
使用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
。
几年前,我写了这个小功能:
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
如果替换版本包含这些令牌,则此版本不会引起问题。
文章标签:mysql , php , security , sql , sql-injection
版权声明:本文为原创文章,版权归 admin 所有,欢迎分享本文,转载请保留出处!
评论已关闭!