是公设辩护准备的发言足以防止SQL注射?
-
02-07-2019 - |
题
让我们说我有这样的代码:
$dbh = new PDO("blahblah");
$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );
辩护文件说:
的参数,以准备好的声明不需要援引;的驱动程序处理它。
是真的我所需要做的,以避免SQL注射?它真的那么容易吗?
你可以假设MySQL如果它有差别。还有,我真的只是好奇有关使用准备好的发言反对SQL注射。在这方面,我不关心的XSS或其他可能的漏洞。
解决方案
简短的回答是 没有,公设辩护准备不会保护你的从所有可能的SQL-注射攻击。对于某些模糊边缘的情况。
我适应 这个答案 谈论公设辩护人...
所长答案不是那么容易。这是基于关闭一次攻击 证明这里.
攻击
因此,让我们开始表示的攻击...
$pdo->query('SET NAMES gbk');
$var = "\xbf\x27 OR 1=1 /*";
$query = 'SELECT * FROM test WHERE name = ? LIMIT 1';
$stmt = $pdo->prepare($query);
$stmt->execute(array($var));
在某些情况下,这将返回超过1行。让我们解剖了什么:
选择一个角色设定
$pdo->query('SET NAMES gbk');
对于这种攻击的工作,我们需要的编码的服务的期待上连接两个编码
'
作为ASCII即0x27
和 有一些角色的最后一个字节是一个ASCII\
即0x5c
.事实证明,有5个这样的编码方式支持在MySQL5.6默认:big5
,cp932
,gb2312
,gbk
和sjis
.我们将选择gbk
在这里。现在,这是非常重要的是要注意使用
SET NAMES
在这里。这一套的角色设定 在服务器上.还有另一种方式这样做,但我们将到那儿很快就好了。有效载荷
有效载荷,我们要利用这个注射始的字节的序列
0xbf27
.在gbk
, 这是个无效的多字节的性质;在latin1
, 它的字符串¿'
.注意到在latin1
和gbk
,0x27
在它自己是一个字'
符。我们已选择这种有效载荷,因为,如果我们叫
addslashes()
在这,我们会插入一个ASCII\
即0x5c
, 之前'
符。所以我们的风了0xbf5c27
, ,这在gbk
是两个字符序列:0xbf5c
随后通过0x27
.或者换句话说, 有效的 字后未转义'
.但我们不用addslashes()
.所以下一步...$stmt->执行()
重要的是要意识到这是一项旨在通过默认不 不 做的真正准备发言。它模仿他们(MySQL).因此,公设辩护人在内部建立的查询string,叫
mysql_real_escape_string()
(MySQL C API function)在每个绑串的价值。C API呼叫
mysql_real_escape_string()
不同之处addslashes()
在于它所知道的连接。所以它可以执行的逃正确的字符集的服务期待。然而,到这一点,客户认为,我们仍然使用latin1
连接的,因为我们从来没有告诉它,否则。我们没有告诉 服务器 我们使用gbk
, 但 客户 仍然认为这是latin1
.因此,该呼叫
mysql_real_escape_string()
插入反斜杠,我们有一个免费挂'
符在我们的"逃脱"的内容!事实上,如果我们是来看看$var
在gbk
字符组,我们看到:縗' OR 1=1 /*
这正是攻击的需要。
查询
这一部分只是一种形式,但是所呈现的查询:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
恭喜你,你刚刚成功地袭击了一个程序中使用公设辩护准备的发言...
简单的解决
现在,这是值得注意的是,你可以防止这种通过禁止仿效准备的发言:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
这将 通常 结果在一个真正准备好的发言稿(即所发送的数据通过在一个单独的分组从查询)。但是,要知道,一项旨在将静静地 后退 以模仿的声明,MySQL不能准备本机:那些它可以是 列 在本手册中,但要注意选择适当的服务器版本)。
正确的解决
这里的问题是,我们没有打电话给C API mysql_set_charset()
而不是的 SET NAMES
.如果我们这样做,我们会被罚款提供我们使用的是一个MySQL释放自2006年以来。
如果你使用一个较早的MySQL释放,然后一个 错误 在 mysql_real_escape_string()
意味着无效多的人物,如那些在我们的有效载荷被视为单一的字节逃脱的目的 即使如果客户已经正确地通知连接的编码 所以这次攻击中仍然会取得成功。该缺陷是固定在MySQL 4.1.20, 5.0.22 和 5.1.11.
但最糟糕的部分是, PDO
没有露C API mysql_set_charset()
直到5.3.6,所以在现有版本 不能 防止这种攻击对于每个可能的命令!它现在作为一个 DSN参数, ,这应当使用 而不是的 SET NAMES
...
的救命之恩
正如我们在一开始所说的,这次袭击工作的数据库连接,必须编码使用的是一个脆弱的字符组。 utf8mb4
是 不脆弱 然而,可以支持 每 Unicode character:所以你可以选择使用,而不是--但它只有已经提供的自MySQL5.5.3.一个选择是 utf8
, ,这也是 不脆弱 并且可以支持整个Unicode 基本多语言面.
或者,可以启用 NO_BACKSLASH_ESCAPES
SQL模式,该模式(除其他事项)改变了的动作 mysql_real_escape_string()
.这个模式启用, 0x27
将被替换 0x2727
而不是 0x5c27
因而逃离的过程 不能 创建有效的字符的任何脆弱的编码,在那里他们没有存在之前(即 0xbf27
仍然是 0xbf27
等等)—所以服务器将仍然拒绝串为无效。然而,看到 @eggyal的答案 对于一个不同的脆弱性,可能会出现使用这SQL模式(尽管没有公设辩护人).
安全的实例
以下的例子是安全的:
mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
因为服务器的期待 utf8
...
mysql_set_charset('gbk');
$var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
因为我们已经正确地设置的字符组使客户和服务器匹配。
$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("\xbf\x27 OR 1=1 /*"));
因为我们已经关闭仿效准备的发言。
$pdo = new PDO('mysql:host=localhost;dbname=testdb;charset=gbk', $user, $password);
$stmt = $pdo->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$stmt->execute(array("\xbf\x27 OR 1=1 /*"));
因为我们设定的角色设定正确。
$mysqli->query('SET NAMES gbk');
$stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1');
$param = "\xbf\x27 OR 1=1 /*";
$stmt->bind_param('s', $param);
$stmt->execute();
因为)的预否真的准备好的发言的时间。
包裹
如果你:
- 使用现代版本的MySQL(晚5.1,所有5.5,5.6,等等) 和 公设辩护人的DSN charset参数(PHP≥5.3.6)
或
- 不要使用一个脆弱的角色设定用于连接的编码(仅使用
utf8
/latin1
/ascii
/etc)
或
- 启用
NO_BACKSLASH_ESCAPES
SQL模式
你是100%安全的。
否则,你的脆弱 即使你使用公设辩护准备的发言...
增编
我已经缓慢地工作上的一个补丁改变的默认不仿效筹备将来的版本的PHP.这个问题,我正在进入的是,大量的测试打破当我这样做。一个问题是仿真的准备仅会扔语法上的错误执行,但真正准备扔上的错误做好准备。因此,可能会导致的问题(并的部分原因测试borking).
其他提示
准备好的语句/参数化查询通常足以阻止 * 上的第一顺序注入。如果在应用程序中的任何其他位置使用未经检查的动态sql,则仍然容易受到第二顺序注入。
二阶注入意味着数据在被包含在查询中之前已经在数据库中循环了一次,并且更难以实现。 AFAIK,你几乎从来没有看到真正设计的二阶攻击,因为攻击者通常更容易进行社交工程,但是你有时会因为额外的良性'
字符而出现二阶错误类似。
当您可以将值存储在稍后用作查询中的文字的数据库中时,您可以完成二阶段注入攻击。例如,假设您在网站上创建帐户时输入以下信息作为新用户名(假设MySQL DB为此问题):
' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '
如果用户名没有其他限制,准备好的语句仍然会确保上述嵌入式查询在插入时不执行,并将值正确存储在数据库中。但是,假设稍后应用程序从数据库中检索您的用户名,并使用字符串连接将该值包含在新查询中。你可能会看到别人的密码。由于用户表中的前几个名称往往是管理员,因此您可能也只是放弃了该服务器场。 (另请注意:这是不以纯文本形式存储密码的另一个原因!)
然后,我们看到,准备好的语句足以用于单个查询,但是它们本身不足以防止整个应用程序中的sql注入攻击,因为它们缺乏强制执行的机制所有对应用程序内数据库的访问都使用安全代码。但是,用作良好应用程序设计的一部分—其中可能包括代码审查或静态分析等实践,或者使用限制动态sql的ORM,数据层或服务层。 预备语句 是解决Sql Injection问题的主要工具。 如果您遵循良好的应用程序设计原则,如果您的数据访问与程序的其余部分分离,则可以轻松地强制执行或审核每个查询是否正确使用参数化。在这种情况下,完全阻止sql注入(第一和第二顺序)。
* 事实证明,当涉及到宽字符时,MySql / PHP对于处理参数只是愚蠢(还可以),而且还有稀有 此处其他高度评价的答案中概述的案例,可以允许注入通过参数化查询。
不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
$dbh = new PDO("blahblah");
$tableToUse = 不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
$dbh = new PDO("blahblah");
$tableToUse = 不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
$dbh = new PDO("blahblah");
$tableToUse = 不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');
将容易受到SQL注入攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
<*>
注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
<*>
上述原因不起作用的原因是 DESC
和 ASC
不是数据。 PDO只能转义数据。其次,你甚至不能在它周围加上'
引号。允许用户选择排序的唯一方法是手动过滤并检查它是 DESC
还是 ASC
。
GET['userTable'];
$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => 不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
<*>
将容易受到SQL注入攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
<*>
注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
<*>
上述原因不起作用的原因是 DESC
和 ASC
不是数据。 PDO只能转义数据。其次,你甚至不能在它周围加上'
引号。允许用户选择排序的唯一方法是手动过滤并检查它是 DESC
还是 ASC
。
REQUEST['username']) );
将容易受到SQL注入攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
<*>
注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
<*>
上述原因不起作用的原因是 DESC
和 ASC
不是数据。 PDO只能转义数据。其次,你甚至不能在它周围加上'
引号。允许用户选择排序的唯一方法是手动过滤并检查它是 DESC
还是 ASC
。
GET['userTable'];
$allowedTables = array('users','admins','moderators');
if (!in_array($tableToUse,$allowedTables))
$tableToUse = 'users';
$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => 不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
$dbh = new PDO("blahblah");
$tableToUse = 不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
<*>
将容易受到SQL注入攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
<*>
注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
<*>
上述原因不起作用的原因是 DESC
和 ASC
不是数据。 PDO只能转义数据。其次,你甚至不能在它周围加上'
引号。允许用户选择排序的唯一方法是手动过滤并检查它是 DESC
还是 ASC
。
GET['userTable'];
$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => 不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
<*>
将容易受到SQL注入攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
<*>
注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
<*>
上述原因不起作用的原因是 DESC
和 ASC
不是数据。 PDO只能转义数据。其次,你甚至不能在它周围加上'
引号。允许用户选择排序的唯一方法是手动过滤并检查它是 DESC
还是 ASC
。
REQUEST['username']) );
将容易受到SQL注入攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
<*>
注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
<*>
上述原因不起作用的原因是 DESC
和 ASC
不是数据。 PDO只能转义数据。其次,你甚至不能在它周围加上'
引号。允许用户选择排序的唯一方法是手动过滤并检查它是 DESC
还是 ASC
。
REQUEST['username']) );
将容易受到SQL注入攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
<*>
注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
<*>
上述原因不起作用的原因是 DESC
和 ASC
不是数据。 PDO只能转义数据。其次,你甚至不能在它周围加上'
引号。允许用户选择排序的唯一方法是手动过滤并检查它是 DESC
还是 ASC
。
GET['userTable'];
$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username');
$stmt->execute( array(':username' => 不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
<*>
将容易受到SQL注入攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
<*>
注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
<*>
上述原因不起作用的原因是 DESC
和 ASC
不是数据。 PDO只能转义数据。其次,你甚至不能在它周围加上'
引号。允许用户选择排序的唯一方法是手动过滤并检查它是 DESC
还是 ASC
。
REQUEST['username']) );
将容易受到SQL注入攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
<*>注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
<*>上述原因不起作用的原因是 DESC
和 ASC
不是数据。 PDO只能转义数据。其次,你甚至不能在它周围加上'
引号。允许用户选择排序的唯一方法是手动过滤并检查它是 DESC
还是 ASC
。
是的,这就足够了。注入类型攻击的工作方式是通过某种方式获得一个解释器(数据库)来评估一些应该是数据的东西,就好像它是代码一样。只有在相同介质中混合代码和数据时才可以这样做(例如,当您将查询构造为字符串时)。
参数化查询通过分别发送代码和数据来工作,因此永远不会在其中找到漏洞。
但您仍然可能容易受到其他注入型攻击。例如,如果您使用HTML页面中的数据,则可能会受到XSS类型攻击。
这还不够(在某些特定情况下)!默认情况下,当使用MySQL作为数据库驱动程序时,PDO使用模拟的预准备语句。在使用MySQL和PDO时,应始终禁用模拟的预准备语句:
$dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
总是应该做的另一件事是设置正确的数据库编码:
$dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');
另请参阅此相关问题:如何防止SQL用PHP注入?
另请注意,这只是关于数据库方面的事情,您在显示数据时仍需要注意自己。例如。使用 htmlspecialchars()
再次使用正确的编码和引用样式。
我个人总是首先对数据进行某种形式的卫生,因为你永远不能信任用户输入,但是当使用占位符/参数绑定时,输入的数据将分别发送到服务器到sql语句,然后绑定在一起。这里的关键是将提供的数据绑定到特定类型和特定用途,并消除任何更改SQL语句逻辑的机会。
Eaven如果你要阻止sql注入前端,使用html或js检查,你必须考虑前端检查是“可绕过的”。
您可以使用前端开发工具(现在内置firefox或chrome)来禁用js或编辑模式。
因此,为了防止SQL注入,在控制器内部清理输入日期后端是正确的。
我建议您使用filter_input()本机PHP函数来清理GET和INPUT值。
如果您想继续使用安全性,对于合理的数据库查询,我建议您使用正则表达式来验证数据格式。 在这种情况下,preg_match()会帮助你! 但要小心!正则表达式引擎并不那么轻巧。仅在必要时使用,否则您的应用程序性能将下降。
安全性有成本,但不要浪费你的表现!
简单的例子:
如果你想仔细检查从GET收到的值是否小于99 如果(!的preg_match( '/ [0-9] {1,2} /')){...}
if (isset($value) && intval($value)) <99) {...}
所以,最后的答案是:“不! PDO Prepared Statements不会阻止所有类型的SQL注入“;它不会阻止意外值,只会意外连接