假设我有这样的代码:
$dbh = new PDO("blahblah"); $stmt = $dbh->prepare('SELECT * FROM users where username = :username'); $stmt->execute( array(':username' => $_REQUEST['username']) );
PDO文件说:
准备语句的参数不需要引用; 司机为你处理.
这真的是我需要做的就是避免SQL注入吗?这真的很容易吗?
如果它有所作为,你可以假设MySQL.另外,我真的只是对使用针对SQL注入的预处理语句感到好奇.在这种情况下,我不关心XSS或其他可能的漏洞.
简短的回答是NO,PDO准备不会保护您免受所有可能的SQL注入攻击.对于某些不起眼的边缘案例.
我正在调整这个答案来谈论PDO ......
答案很长并不容易.它基于此处演示的攻击.
攻击那么,让我们从展示攻击开始......
$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
.事实证明,会默认在MySQL 5.6支持5个这样的编码:big5
,cp932
,gb2312
,gbk
和sjis
.我们会gbk
在这里选择.
现在,注意SET NAMES
这里的使用非常重要.这会将字符集设置为"服务器".还有另一种方法,但我们很快就会到达那里.
有效载荷
我们将用于此注入的有效负载从字节序列开始0xbf27
.在gbk
,这是一个无效的多字节字符; 在latin1
,它是字符串¿'
.请注意,在latin1
和中 gbk
,0x27
它本身就是一个文字'
字符.
我们选择了这个有效载荷,因为,如果我们叫addslashes()
上他,我们就插入一个ASCII \
即0x5c
,在之前'
的字符.所以我们结束了0xbf5c27
,这gbk
是一个两个字符的序列:0xbf5c
接下来0x27
.或者换句话说,一个有效的字符后跟一个未转义的字符'
.但我们没有使用addslashes()
.那么下一步......
$ stmt->的execute()
这里要认识到的重要一点是,默认情况下PDO 不会执行真正准备好的语句.它模仿它们(对于MySQL).因此,PDO在内部构建查询字符串,mysql_real_escape_string()
在每个绑定的字符串值上调用(MySQL C API函数).
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准备语句成功攻击了一个程序......
简单修复现在,值得注意的是,您可以通过禁用模拟的预准备语句来防止这种情况:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
这通常会导致真正准备好的语句(即数据在查询的单独数据包中发送).但是,请注意,PDO将无声地回退到模拟MySQL本身无法准备的语句:可以在手册中列出的那些语句,但要注意选择适当的服务器版本).
正确的修复这里的问题是我们没有调用C API mysql_set_charset()
而不是SET NAMES
.如果我们这样做,我们会很好,只要我们从2006年开始使用MySQL版本.
如果您使用的是较早的MySQL版本,那么错误的mysql_real_escape_string()
意思是无效的多字节字符,例如那些在我们的有效载荷被视为单字节转义的目的,即使客户端已正确通知连接编码的,因此这种攻击仍然成功.该错误是固定在MySQL 4.1.20,5.0.22和5.1.11.
但最糟糕的是,直到5.3.6 PDO
才公开C API mysql_set_charset()
,因此在以前的版本中,它无法阻止每次可能命令的攻击!它现在作为DSN参数公开,应该使用它代替 SET NAMES
...
正如我们在开始时所说的,为了使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码. utf8mb4
是不容易,但可以支持所有的 Unicode字符:所以你可以选择使用的是代替,但它只是从MySQL 5.5.3可用.另一种选择是utf8
,它也不易受攻击,可以支持整个Unicode 基本多语言平面.
或者,您可以启用NO_BACKSLASH_ESCAPES
SQL模式,其中(除其他外)改变了操作mysql_real_escape_string()
.启用此模式后,0x27
将替换为0x2727
而不是0x5c27
因为转义进程无法在以前不存在的任何易受攻击的编码中创建有效字符(即0xbf27
仍然是0xbf27
等等) - 因此服务器仍将拒绝该字符串为无效.但是,请参阅@ eggyal的答案,了解使用此SQL模式可能产生的其他漏洞(尽管不使用PDO).
以下示例是安全的:
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();
因为MySQLi一直都做真实的准备语句.
包起来如果你:
使用MySQL的现代版本(后期5.1,全部5.5,5.6等)和 PDO的DSN字符集参数(在PHP≥5.3.6中)
要么
不要使用易受攻击的字符集进行连接编码(仅使用utf8
/ latin1
/ ascii
/ etc)
要么
启用NO_BACKSLASH_ESCAPES
SQL模式
你100%安全.
否则,即使您正在使用PDO准备语句,您也很容易受到攻击......
附录我一直在慢慢研究修补程序,将默认设置更改为不模拟未来版本的PHP.我遇到的问题是,当我这样做时,很多测试都会中断.一个问题是模拟的准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误.这可能会导致问题(并且是测试的原因之一).
准备好的语句/参数化查询通常足以阻止对该语句的第一顺序注入*.如果在应用程序的任何其他位置使用未经检查的动态sql,则仍然容易受到二阶注入攻击.
二阶注入意味着数据在被包含在查询中之前已经在数据库中循环了一次,并且更难以实现.AFAIK,你几乎从来没有看到真正设计的二阶攻击,因为攻击者通常更容易进行社交工程,但是你有时会因为额外的良性'
角色或类似情况而出现二阶错误.
如果可以将值存储在稍后用作查询中的文字的数据库中,则可以完成二阶注入攻击.例如,假设您在网站上创建帐户时输入以下信息作为新用户名(假设MySQL DB为此问题):
' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '
如果用户名没有其他限制,则预准备语句仍将确保上述嵌入式查询在插入时不执行,并将值正确存储在数据库中.但是,假设稍后应用程序从数据库中检索您的用户名,并使用字符串连接将该值包含在新查询中.你可能会看到别人的密码.由于用户表中的前几个名称往往是管理员,因此您可能也只是放弃了该服务器场.(另请注意:这是不以明文形式存储密码的另一个原因!)
然后,我们看到,准备好的语句足以用于单个查询,但是它们本身并不足以防止整个应用程序中的SQL注入攻击,因为它们缺乏一种机制来强制应用程序中对数据库的所有访问都使用安全的代码.然而,用作良好的应用程序设计部分-其可以包括做法,如代码评审或静态分析,或使用一个ORM的,数据层,或者服务层限制动态SQL - 准备语句是用于解决SQL注入的主要工具问题.如果您遵循良好的应用程序设计原则,使您的数据访问与程序的其余部分分离,则可以轻松地强制执行或审核每个查询是否正确使用参数化.在这种情况下,完全阻止sql注入(第一和第二顺序).
*事实证明,当涉及到宽字符时,MySql/PHP对于处理参数只是愚蠢的,并且在这里另一个高度投票的答案中仍然有一个罕见的案例可以允许注入滑过参数化查询.
不,他们并不总是.
这取决于您是否允许将用户输入放在查询本身中.例如:
$dbh = new PDO("blahblah"); $tableToUse = $_GET['userTable']; $stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username'); $stmt->execute( array(':username' => $_REQUEST['username']) );
将容易受到SQL注入的攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据.这里正确的答案是使用某种过滤/验证,如:
$dbh = new PDO("blahblah"); $tableToUse = $_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' => $_REQUEST['username']) );
注意:您不能使用PDO绑定超出DDL(数据定义语言)的数据,即这不起作用:
$stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData');
以上不起作用的原因是因为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');
另请参阅此相关问题:如何在PHP中阻止SQL注入?
另请注意,这只是关于数据库方面的事情,您在显示数据时仍需要注意自己.例如,htmlspecialchars()
再次使用正确的编码和引用样式.
我个人总是首先对数据进行某种形式的卫生,因为你永远不会信任用户输入,但是当使用占位符/参数绑定时,输入的数据将分别发送到服务器到sql语句,然后绑定在一起.这里的关键是将提供的数据绑定到特定类型和特定用途,并消除任何更改SQL语句逻辑的机会.