关于php:PDO准备好的语句是否足以阻止SQL注入?

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');

    为了使这种攻击起作用,我们需要服务器在连接上期望的编码,既将'编码为ASCII格式,即0x27,也将最终字节为ASCII \的字符编码为0x5c。结果表明,MySQL5.6默认支持5种编码:big5cp932gb2312gbksjis。我们在这里选择gbk。好的。

    现在,注意这里使用SET NAMES非常重要。这将设置服务器上的字符集。还有另一种方法,但我们很快就会到的。好的。

  • 有效载荷好的。

    我们将用于此注入的有效负载从字节序列0xbf27开始。在gbk中,这是一个无效的多字节字符;在latin1中,它是字符串?'。注意,在latin1gbk中,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()的调用插入了反斜杠,我们的"转义"内容中有一个自由挂起的'字符!事实上,如果我们看gbk字符集中的$var,我们会看到:好的。

    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_set_charset(),而不是SET NAMES。如果我们这样做了,如果我们从2006年开始使用MySQL版本的话,那就没问题了。好的。

    如果您使用的是早期的mysql版本,那么mysql_real_escape_string()中的一个bug意味着无效的多字节字符(如我们的有效负载中的那些)被视为单字节以进行转义,即使客户机被正确地通知了连接编码,因此这种攻击仍然会成功。在MySQL4.1.20、5.0.22和5.1.11中修复了这个bug。好的。

    但最糟糕的是,在5.3.6之前,PDO没有公开mysql_set_charset()的C API,所以在以前的版本中,它不能阻止对每个可能的命令进行攻击!它现在作为一个DSN参数公开,应该用它来代替SET NAMES。好的。拯救恩典

    正如我们在开始时所说,要使此攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。utf8mb4不易受攻击,但可以支持每个Unicode字符:因此您可以选择使用它来代替—但它只有在MySQL5.5.3之后才可用。另一种选择是utf8,它也不易受攻击,可以支持整个Unicode基本多语言平面。好的。

    或者,您可以启用NO_BACKSLASH_ESCAPESSQL模式,它(除其他外)会改变mysql_real_escape_string()的操作。启用此模式后,0x27将替换为0x2727,而不是0x5c27,因此转义过程无法在以前不存在的任何易受攻击的编码中创建有效字符(即0xbf27仍然是0xbf27等);mdash;因此服务器仍将拒绝将字符串视为无效。但是,请参阅@eggyal的答案,了解使用此SQL模式(尽管不使用PDO)可能产生的不同漏洞。好的。安全实例

    以下示例是安全的:好的。

    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");

    因为服务器正在等待utf8…好的。

    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_ESCAPESSQL模式

    你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');

    以上不起作用的原因是DESCASC不是数据。PDO只能对数据进行转义。其次,你甚至不能用'的报价来绕过它。允许用户选择排序的唯一方法是手动筛选并检查它是DESC还是ASC


    不,这还不够(在某些特定情况下)!默认情况下,当使用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注入?

    还要注意的是,这只是关于数据库方面的事情,在显示数据时,您仍然需要观察自己。例如,用正确的编码和引用样式再次使用htmlspecialchars()


    是的,足够了。注入类型攻击的工作方式是通过某种方式让解释器(数据库)对应该是数据的东西进行评估,就像它是代码一样。只有将代码和数据混合在同一介质中(例如,将查询构造为字符串时),这才是可能的。

    参数化查询通过单独发送代码和数据来工作,因此永远不可能找到其中的漏洞。

    但是,您仍然可能容易受到其他注入类型的攻击。例如,如果使用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/')…是天堂

    1
    if (isset($value) && intval($value)) <99) {...}

    所以,最后的答案是:"不!PDO准备好的语句不会阻止所有类型的SQL注入";它不会阻止意外的值,只是意外的连接