首页 文章

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

提问于
浏览
578

假设我有这样的代码:

$dbh = new PDO("blahblah");

$stmt = $dbh->prepare('SELECT * FROM users where username = :username');
$stmt->execute( array(':username' => $_REQUEST['username']) );

PDO文件说:

不需要引用准备语句的参数;司机为你处理 .

Is that truly all I need to do to avoid SQL injections? Is it really that easy?

如果它有所作为,你可以假设MySQL . 另外,我真的只是对使用针对SQL注入的预处理语句感到好奇 . 在这种情况下,我不关心XSS或其他可能的漏洞 .

7 回答

  • 715

    简短的回答是 NO ,PDO准备不会保护您免受所有可能的SQL注入攻击 . 对于某些不起眼的边缘案例 .

    我正在调整this answer谈论PDO ......

    很长的答案是基于攻击demonstrated here .

    攻击

    那么,让我们从展示攻击开始......

    $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行 . 让我们剖析一下这里发生了什么:

    • Selecting a Character Set
    $pdo->query('SET NAMES gbk');
    

    为了使这个攻击起作用,我们需要服务器在连接上所期望的编码以ASCII编码 ' ,即 0x27 ,并且要有一些字符,其最后一个字节是ASCII \ ,即 0x5c . 事实证明,默认情况下,MySQL 5.6中支持5种此类编码: big5cp932gb2312gbksjis . 我们在这里选择 gbk .

    现在,注意在这里使用 SET NAMES 非常重要 . 这将设置字符集 ON THE SERVER . 还有另一种方法,但我们很快就会到达那里 .

    • The Payload

    我们将用于此注入的有效负载从字节序列 0xbf27 开始 . 在 gbk 中,这是一个无效的多字节字符;在 latin1 中,它是字符串 ¿' . 请注意,在 latin1 and gbk 中, 0x27 本身就是一个文字 ' 字符 .

    我们已选择此有效负载,因为如果我们在其上调用 addslashes() ,我们将在 ' 字符之前插入ASCII \ ,即 0x5c . 所以我们最终得到了 0xbf5c27 ,它在 gbk 中是一个两个字符的序列: 0xbf5c 后跟 0x27 . 或换句话说,一个有效的字符后跟一个未转义的 ' . 但是我们没有使用 addslashes() . 那么下一步......

    • $stmt->execute()

    这里要认识到的重要一点是,默认情况下PDO会 NOT 执行真正准备好的语句 . 它模仿它们(对于MySQL) . 因此,PDO在内部构建查询字符串,在每个绑定的字符串值上调用 mysql_real_escape_string() (MySQL C API函数) .

    mysql_real_escape_string() 的C API调用与 addslashes() 的不同之处在于它知道连接字符集 . 因此它可以为服务器期望的字符集正确执行转义 . 但是,到目前为止,客户认为我们仍在使用 latin1 进行连接,因为我们从未告诉过它 . 我们告诉服务器我们正在使用 gbk ,但客户端仍然认为它是 latin1 .

    因此,调用 mysql_real_escape_string() 会插入反斜杠,我们的"escaped"内容中有一个自由挂起的 ' 字符!事实上,如果我们在 gbk 字符集中查看 $var ,我们会看到:

    縗' OR 1=1 /*
    

    这正是攻击所需要的 .

    • The Query

    这部分只是一种形式,但这里是渲染的查询:

    SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1
    

    恭喜,您刚刚使用PDO Prepared Statements成功攻击了一个程序......

    简单修复

    现在,值得注意的是,您可以通过禁用模拟的预准备语句来防止这种情况:

    $pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
    

    这通常会产生一个真正准备好的语句(即数据是在查询的单独数据包中发送的) . 但是,请注意,PDO将默默地模拟MySQL无法本机准备的语句:手册中可能是listed,但要注意选择适当的服务器版本) .

    正确修复

    这里的问题是我们没有 mysql_set_charset() 而不是 SET NAMES . 如果我们这样做,我们会很好,只要我们自2006年以来使用MySQL版本 .

    如果您使用的是早期的MySQL版本,则 mysql_real_escape_string() 中的bug意味着无效的多字节字符(例如我们的有效负载中的字符)被视为单个字节以用于转义目的,即使客户端已正确通知连接编码,因此此攻击也是如此仍然会成功 . 该错误已在MySQL 4.1.205.0.225.1.11中修复 .

    但最糟糕的是 PDO 直到5.3.6才公开 mysql_set_charset() 的C API,所以在以前的版本中它 cannot 阻止了对每个可能命令的攻击!它现在暴露为DSN parameter,应该使用 instead of SET NAMES ...

    拯救恩典

    正如我们在开始时所说的,为了使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码 . utf8mb4不易受攻击,但可以支持每个Unicode字符:所以你可以选择使用它 - 但它只能从MySQL 5.5.3开始使用 . 另一种选择是utf8,也不是易受攻击并且可以支持整个Unicode Basic Multilingual Plane .

    或者,您可以启用NO_BACKSLASH_ESCAPES SQL模式,该模式(其中包括)会改变 mysql_real_escape_string() 的操作 . 启用此模式后, 0x27 将替换为 0x2727 而不是 0x5c27 ,因此转义进程无法在以前不存在的任何易受攻击的编码中创建有效字符(即 0xbf27 仍为 0xbf27 等) - 因此服务器仍将拒绝该字符串无效 . 但是,请参阅@eggyal's answer以了解使用此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等) AND PDO的DSN字符集参数(在PHP≥5.3.6中)

    OR

    • 不要使用易受攻击的字符集进行连接编码(仅使用 utf8 / latin1 / ascii / etc)

    OR

    • 启用 NO_BACKSLASH_ESCAPES SQL模式

    你是100%安全的 .

    否则,你很脆弱 even though you're using PDO Prepared Statements...

    附录

    我一直在慢慢研究修补程序,将默认设置更改为不模拟准备未来版本的PHP . 我遇到的问题是,当我这样做时,很多测试都会中断 . 一个问题是模拟的准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误 . 因此,这可能会导致问题(并且是测试结果的一部分) .

  • 24

    准备好的语句/参数化查询通常足以阻止对该语句的第一顺序注入* . 如果在应用程序中的任何其他位置使用未经检查的动态sql,则仍然容易受到二阶注入攻击 .

    二阶注入意味着数据在被包含在查询中之前已经在数据库中循环了一次,并且更难以实现 . AFAIK,你几乎从来没有看到真正设计的二阶攻击,因为攻击者通常更容易进行社交工程,但是你有时会因为额外的良性 ' 字符或类似情况而出现二阶错误 .

    当您可以将值存储在稍后用作查询中的文字的数据库中时,您可以完成二阶注入攻击 . 例如,假设您在网站上创建帐户时输入以下信息作为新用户名(假设MySQL DB为此问题):

    ' + (SELECT UserName + '_' + Password FROM Users LIMIT 1) + '
    

    如果用户名没有其他限制,则预准备语句仍将确保上述嵌入式查询在插入时不执行,并将值正确存储在数据库中 . 但是,假设稍后应用程序从数据库中检索您的用户名,并使用字符串连接将该值包含在新查询中 . 你可能会看到别人的密码 . 由于用户表中的前几个名称往往是管理员,因此您可能也只是放弃了该服务器场 . (另请注意:这是不以明文形式存储密码的另一个原因!)

    然后,我们看到,准备好的语句足以用于单个查询,但是它们本身足以防止整个应用程序中的SQL注入攻击,因为它们缺少一种机制来强制应用程序中对数据库的所有访问都使用安全的代码 . 但是,作为良好应用程序设计的一部分 - 可能包括代码审查或静态分析等实践,或者使用限制动态sql的ORM,数据层或服务层 - prepared statements are the primary tool for solving the Sql Injection problem. 如果您遵循良好的应用程序设计原则,例如如果您的数据访问与程序的其余部分分离,则可以轻松地强制执行或审核每个查询是否正确使用参数化 . 在这种情况下,完全阻止sql注入(第一和第二顺序) .


    *事实证明,当涉及宽字符时,MySql / PHP对于处理参数只是愚蠢的,并且在这里另一个高度投票的答案中仍然有一个罕见的案例可以允许注入通过参数化查询 .

  • 24

    我个人总是首先对数据运行某种形式的卫生,因为你永远不能信任用户输入,但是当使用占位符/参数绑定时,输入的数据被单独发送到服务器到sql语句,然后绑定在一起 . 这里的关键是将提供的数据绑定到特定类型和特定用途,并消除任何更改SQL语句逻辑的机会 .

  • 9

    Eaven如果要防止sql注入前端,使用html或js检查,你必须考虑前端支票是“可绕过的” .

    您可以使用前端开发工具(现在内置firefox或chrome)来禁用js或编辑模式 .

    因此,为了防止SQL注入,在控制器内清理输入日期后端是正确的 .

    我建议您使用filter_input()本机PHP函数来清理GET和INPUT值 .

    如果您想继续安全,对于合理的数据库查询,我建议您使用正则表达式来验证数据格式 . 在这种情况下,preg_match()会帮助你!但要小心!正则表达式引擎并不那么轻巧 . 仅在必要时使用,否则您的应用程序性能将下降 .

    安全性有成本,但不要浪费你的表现!

    简单的例子:

    如果你想仔细检查从GET收到的值是否是一个数字,如果(!preg_match('/ [0-9] {1,2} /'))的重量超过99

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

    所以,最后的答案是:“不!PDO Prepared Statements不会阻止所有类型的SQL注入”;它不会阻止意外的值,只会意外连接

  • 40

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

    另请参阅此相关问题:How can I prevent SQL injection in PHP?

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

  • 497

    不,他们并不总是 .

    这取决于您是否允许将用户输入放在查询本身中 . 例如:

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

    上述原因不起作用的原因是 DESCASC 不是数据 . PDO只能逃避数据 . 其次,你甚至不能在它周围加上 ' 引号 . 允许用户选择排序的唯一方法是手动过滤并检查它是 DESC 还是 ASC .

  • -1

    是的,这就足够了 . 注入类型攻击的工作方式是通过某种方式获得一个解释器(数据库)来评估一些应该是数据的东西,就好像它是代码一样 . 只有在同一介质中混合代码和数据时才可以这样做(例如,当您将查询构造为字符串时) .

    参数化查询通过分别发送代码和数据来工作,因此永远不可能找到一个漏洞 .

    尽管如此,您仍然可能容易受到其他注入型攻击 . 例如,如果您使用HTML页面中的数据,则可能会受到XSS类型攻击 .

相关问题