PDO准备好的语句是否足以阻止SQL注入?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了PDO准备好的语句是否足以阻止SQL注入?相关的知识,希望对你有一定的参考价值。
假设我有这样的代码:
$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注入攻击。对于某些不起眼的边缘案例。
我正在调整this answer谈论PDO ......
答案很长并不容易。它基于攻击demonstrated here。
The Attack
那么,让我们从展示攻击开始......
$pdo->query('SET NAMES gbk');
$var = "xbfx27 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函数)。 对mysql_real_escape_string()
的C API调用与addslashes()
的不同之处在于它知道连接字符集。因此它可以为服务器期望的字符集正确执行转义。然而,到目前为止,客户认为我们仍在使用latin1
进行连接,因为我们从未告诉过它。我们告诉服务器我们正在使用gbk
,但客户端仍然认为它是latin1
。 因此,对mysql_real_escape_string()
的调用插入反斜杠,我们在“转义”内容中有一个免费悬挂的'
字符!事实上,如果我们在$var
字符集中查看gbk
,我们会看到: 缞'或1 = 1 / * 这正是攻击所需要的。 - 查询
这部分只是一种形式,但这里是渲染的查询:
SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
恭喜,您刚刚使用PDO准备语句成功攻击了一个程序......
The Simple Fix
现在,值得注意的是,您可以通过禁用模拟的预准备语句来防止这种情况:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
这通常会产生一个真实的预处理语句(即数据是在查询的单独数据包中发送的)。但是,请注意,PDO将默默地fallback模拟MySQL无法本机准备的语句:那些可以在手册中使用listed,但要注意选择适当的服务器版本)。
The Correct Fix
这里的问题是我们没有调用C API的mysql_set_charset()
而不是SET NAMES
。如果我们这样做,我们会很好,只要我们从2006年开始使用MySQL版本。
如果您正在使用早期的MySQL版本,那么bug中的mysql_real_escape_string()
意味着无效的多字节字符(例如我们的有效负载中的那些字符)被视为单个字节以用于转义目的,即使客户端已正确通知连接编码因此此攻击仍然会成功。该错误已在MySQL 4.1.20,5.0.22和5.1.11中修复。
但最糟糕的是,PDO
没有公开Qazxswpoi的C API直到5.3.6,所以在以前的版本中它无法阻止这种攻击的每一个可能的命令!它现在暴露为mysql_set_charset()
,应该用来代替DSN parameter ......
The Saving Grace
正如我们在开始时所说的,为了使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。 SET NAMES
不易受攻击,但可以支持每个Unicode字符:所以你可以选择使用它 - 但它只能从MySQL 5.5.3开始使用。另一种选择是utf8mb4
,它也不易受攻击,可以支持整个Unicode utf8
。
或者,您可以启用Basic Multilingual Plane SQL模式,其中(除其他外)改变了NO_BACKSLASH_ESCAPES
的操作。启用此模式后,mysql_real_escape_string()
将替换为0x27
而不是0x2727
,因此转义过程无法在以前不存在的任何易受攻击的编码中创建有效字符(即0x5c27
仍然是0xbf27
等) - 所以服务器仍然会拒绝该字符串无效。但是,请参阅0xbf27
,了解使用此SQL模式可能产生的其他漏洞(尽管不使用PDO)。
Safe Examples
以下示例是安全的:
@eggyal's answer
因为服务器期待mysql_query('SET NAMES utf8');
$var = mysql_real_escape_string("xbfx27 OR 1=1 /*");
mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1");
...
utf8
因为我们已正确设置字符集,因此客户端和服务器匹配。
mysql_set_charset('gbk');
$var = mysql_real_escape_string("xbfx27 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("xbfx27 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("xbfx27 OR 1=1 /*"));
因为MySQLi一直都做真实的准备语句。
Wrapping Up
如果你:
- 使用MySQL的现代版本(后期5.1,全部5.5,5.6等)和PDO的DSN字符集参数(在php≥5.3.6中)
要么
- 不要使用易受攻击的字符集进行连接编码(您只使用
$mysqli->query('SET NAMES gbk'); $stmt = $mysqli->prepare('SELECT * FROM test WHERE name = ? LIMIT 1'); $param = "xbfx27 OR 1=1 /*"; $stmt->bind_param('s', $param); $stmt->execute();
/utf8
/latin1
/ etc)
要么
- 启用
ascii
SQL模式
你100%安全。
否则,即使您正在使用PDO准备语句,您也很容易受到攻击......
Addendum
我一直在慢慢研究修补程序,将默认设置更改为不模拟未来版本的PHP。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是模拟的准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。这可能会导致问题(并且是测试的原因之一)。
准备好的语句/参数化查询通常足以阻止对该语句的第一顺序注入*。如果在应用程序的任何其他位置使用未经检查的动态sql,则仍然容易受到二阶注入攻击。
二阶注入意味着数据在被包含在查询中之前已经在数据库中循环了一次,并且更难以实现。 AFAIK,你几乎从来没有看到真正设计的二阶攻击,因为攻击者通常更容易进行社交工程,但你有时会因为额外的良性NO_BACKSLASH_ESCAPES
字符或类似情况而出现二阶错误。
如果可以将值存储在稍后用作查询中的文字的数据库中,则可以完成二阶注入攻击。例如,假设您在网站上创建帐户时输入以下信息作为新用户名(假设MySQL DB为此问题):
'
如果用户名没有其他限制,则预准备语句仍将确保上述嵌入式查询在插入时不执行,并将值正确存储在数据库中。但是,假设稍后应用程序从数据库中检索您的用户名,并使用字符串连接将该值包含在新查询中。你可能会看到别人的密码。由于用户表中的前几个名称往往是管理员,因此您可能也只是放弃了该服务器场。 (另请注意:这是不以明文形式存储密码的另一个原因!)
我们看到,准备好的语句足以用于单个查询,但是它们本身并不足以防止整个应用程序中的SQL注入攻击,因为它们缺乏一种机制来强制所有对应用程序内数据库的访问都使用安全的代码。但是,用作良好应用程序设计的一部分 - 可能包括代码检查或静态分析等实践,或者使用ORM,数据层或限制动态sql准备语句的服务层是解决Sql注入的主要工具问题。如果您遵循良好的应用程序设计原则,使您的数据访问与程序的其余部分分离,则可以轻松地强制执行或审核每个查询是否正确使用参数化。在这种情况下,完全阻止sql注入(第一和第二顺序)。
*事实证明,当涉及宽字符时,MySql / PHP对于处理参数是愚蠢的(好吧,那些),并且在' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '
中仍然有一个罕见的案例可以允许注入通过参数化查询。
不,他们并不总是。
这取决于您是否允许将用户输入放在查询本身中。例如:
other highly-voted answer here
将容易受到SQL注入的攻击,并且在此示例中使用预准备语句将不起作用,因为用户输入用作标识符,而不是数据。这里正确的答案是使用某种过滤/验证,如:
$dbh = new PDO("blahblah");
$tableToUse = $_GET['userTable'];
$stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . '以上是关于PDO准备好的语句是否足以阻止SQL注入?的主要内容,如果未能解决你的问题,请参考以下文章
在 MySQL 中使用准备好的语句可以防止 SQL 注入攻击吗?