Are PDO prepared statements sufficient to prevent SQL injection?
假设我有这样的代码:
1 2 3 4 | $dbh = new PDO("blahblah"); $stmt = $dbh->prepare('SELECT * FROM users where username = :username'); $stmt->execute( array(':username' => $_REQUEST['username']) ); |
PDO文件显示:
The parameters to prepared statements don't need to be quoted; the driver handles it for you.
这真的是避免SQL注入所需要做的全部工作吗?真的那么容易吗?
如果MySQL有所不同,您可以假设它。另外,我对准备好的语句用于SQL注入也很好奇。在这种情况下,我不关心XSS或其他可能的漏洞。
简短的回答是不,PDO准备不会保护您免受所有可能的SQL注入攻击。对于某些模糊的边缘情况。好的。
我正在调整这个答案来讨论PDO…好的。
答案不那么简单。这是基于这里演示的攻击。好的。进攻
那么,让我们从展示攻击开始……好的。
1 2 3 4 5 | $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'); |
为了使这种攻击起作用,我们需要服务器在连接上期望的编码,既将
现在,注意这里使用
有效载荷好的。
我们将用于此注入的有效负载从字节序列
我们选择这个有效载荷是因为,如果我们在它上面调用
$stmt->execute()。好的。
这里要认识到的重要一点是,默认情况下,PDO不执行真正的准备语句。它模仿它们(对于MySQL)。因此,pdo在内部构建查询字符串,对每个绑定字符串值调用
对
因此,对
1 | 縗' OR 1=1 /* |
这正是攻击所需要的。好的。
查询好的。
这部分只是一个形式,但这里是呈现的查询:好的。
1 | SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1 |
恭喜您,您刚刚成功地使用PDO准备的语句攻击了一个程序…好的。简单固定
现在,值得注意的是,您可以通过禁用模拟的准备语句来防止这种情况:好的。
1 | $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); |
这通常会导致一个真正准备好的语句(即,数据从查询中以单独的数据包发送)。但是,请注意,PDO会悄悄地回退到模仿MySQL本机无法准备的语句:那些可以在手册中列出的语句,但是要注意选择适当的服务器版本)。好的。正确的定位
这里的问题是,我们没有将C API称为
如果您使用的是早期的mysql版本,那么
但最糟糕的是,在5.3.6之前,
正如我们在开始时所说,要使此攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。
或者,您可以启用
以下示例是安全的:好的。
1 2 3 | 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"); |
因为服务器正在等待
1 2 3 | mysql_set_charset('gbk'); $var = mysql_real_escape_string("\xbf\x27 OR 1=1 /*"); mysql_query("SELECT * FROM test WHERE name = '$var' LIMIT 1"); |
因为我们已经正确地设置了字符集,以便客户机和服务器匹配。好的。
1 2 3 4 | $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 /*")); |
因为我们关闭了模拟的准备语句。好的。
1 2 3 | $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 /*")); |
因为我们已经正确设置了字符集。好的。
1 2 3 4 5 | $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 charset参数(在php中≥5.3.6)
或好的。
- 不要使用易受攻击的字符集进行连接编码(只使用
utf8 /latin1 /ascii 等)
或好的。
- 启用
NO_BACKSLASH_ESCAPES SQL模式
你100%安全。好的。
否则,即使使用PDO准备好的语句,您也很容易受到攻击…好的。补遗
我一直在缓慢地开发一个补丁,将默认值改为不模拟,为将来的PHP版本做准备。我遇到的问题是,当我这样做时,很多测试都会中断。一个问题是,模拟准备只在执行时引发语法错误,而真正准备将在准备时引发错误。因此,这可能会导致问题(也是测试产生的原因之一)。好的。好啊。
准备好的语句/参数化查询通常足以防止对该语句进行一阶注入*。如果在应用程序中的其他任何地方使用未经检查的动态SQL,则仍然容易受到二阶注入的攻击。
二阶注入意味着数据在包含在查询中之前已经在数据库中循环了一次,而且很难实现。对于Afaik,你几乎看不到真正的工程二级攻击,因为攻击者通常更容易进行社会工程,但有时会因为额外的良性
当您可以使一个值存储在一个数据库中时,您就可以完成一个二阶注入攻击,该数据库后来被用作查询中的文本。例如,假设您在网站上创建帐户时输入以下信息作为新的用户名(假设此问题使用mysql db):
1 | ' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + ' |
如果用户名没有其他限制,那么准备好的语句仍然可以确保在插入时不执行上述嵌入查询,并将值正确存储在数据库中。但是,假设稍后应用程序从数据库中检索您的用户名,并使用字符串连接将该值包括在新查询中。你可能会看到别人的密码。由于"用户"表中的前几个名称往往是管理员,因此您也可能刚刚放弃了农场。(另请注意:这也是不以纯文本形式存储密码的另一个原因!)
因此,我们看到,准备好的语句足以用于单个查询,但它们本身不足以防止整个应用程序中的SQL注入攻击,因为它们缺乏一种机制来强制应用程序中对数据库的所有访问都使用安全代码。但是,作为良好应用程序设计的一部分使用,这可能包括代码审查或静态分析等实践,或者使用限制动态SQL的ORM、数据层或服务层,准备好的语句是解决SQL注入问题的主要工具。如果您遵循良好的应用程序设计原则,从而使数据访问与程序的其余部分分离,那么很容易强制或审计每个查询都正确地使用参数化。在这种情况下,SQL注入(一阶和二阶)被完全阻止。
*事实证明,当涉及宽字符时,mysql/php(好的,were)对处理参数很愚蠢,并且在这里的另一个高度投票的答案中仍然有一个罕见的情况,可以允许注入通过参数化查询滑入。
不,他们并不总是这样。
这取决于是否允许将用户输入放在查询本身中。例如:
1 2 3 4 5 6 | $dbh = new PDO("blahblah"); $tableToUse = $_GET['userTable']; $stmt = $dbh->prepare('SELECT * FROM ' . $tableToUse . ' where username = :username'); $stmt->execute( array(':username' => $_REQUEST['username']) ); |
很容易受到SQL注入的攻击,在本例中使用准备好的语句将不起作用,因为用户输入被用作标识符,而不是数据。这里的正确答案是使用某种过滤/验证,比如:
1 2 3 4 5 6 7 8 9 | $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(数据定义语言)之外的数据,即这不起作用:
1 | $stmt = $dbh->prepare('SELECT * FROM foo ORDER BY :userSuppliedData'); |
以上不起作用的原因是
不,这还不够(在某些特定情况下)!默认情况下,当使用MySQL作为数据库驱动程序时,PDO使用模拟的准备语句。使用mysql和pdo时,应始终禁用模拟的准备语句:
1 | $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); |
另一件应该经常做的事情是设置正确的数据库编码:
1 | $dbh = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass'); |
另请参见这个相关的问题:如何在PHP中防止SQL注入?
还要注意的是,这只是关于数据库方面的事情,在显示数据时,您仍然需要观察自己。例如,用正确的编码和引用样式再次使用
是的,足够了。注入类型攻击的工作方式是通过某种方式让解释器(数据库)对应该是数据的东西进行评估,就像它是代码一样。只有将代码和数据混合在同一介质中(例如,将查询构造为字符串时),这才是可能的。
参数化查询通过单独发送代码和数据来工作,因此永远不可能找到其中的漏洞。
但是,您仍然可能容易受到其他注入类型的攻击。例如,如果使用HTML页面中的数据,则可能受到XSS类型攻击。
我个人总是先对数据运行某种形式的清理,因为您永远不能信任用户输入,但是当使用占位符/参数绑定时,输入的数据将分别发送到服务器上的SQL语句,然后绑定在一起。这里的关键是,它将所提供的数据绑定到特定的类型和特定的用途,并消除了更改SQL语句逻辑的任何机会。
如果要使用HTML或JS检查来防止SQL注入前端,那么必须考虑前端检查是"可绕过的"。
您可以禁用JS或使用前端开发工具(现在内置于火狐或Chrome)编辑模式。
因此,为了防止SQL注入,对控制器内部的输入日期后端进行清理是正确的。
我建议您使用filter_input()本机php函数来清理get和input值。
如果您想继续进行安全性方面的工作,对于明智的数据库查询,我建议您使用正则表达式来验证数据格式。在这种情况下,preg_match()将帮助您!但是要小心!Regex引擎不太轻。只有在必要时才使用,否则您的应用性能会下降。
安全是有代价的,但不要浪费你的表现!
简单例子:
如果您想再次检查从get接收的值是否为数字,小于99如果(!)preg_match('/[0-9]1,2/')…是天堂
所以,最后的答案是:"不!PDO准备好的语句不会阻止所有类型的SQL注入";它不会阻止意外的值,只是意外的连接