假设我有这样的代码:

$dbh = new PDO("blahblah");

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

PDO文档说:

预处理语句的参数不需要加引号;司机会帮你处理的。

这真的是避免SQL注入所需要做的一切吗?真的那么简单吗?

如果MySQL有影响,你可以假设它是MySQL。另外,我对使用预处理语句对抗SQL注入真的很好奇。在这种情况下,我不关心XSS或其他可能的漏洞。


当前回答

不,他们不总是这样。

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

$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。

其他回答

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

你可以禁用js或者使用前端开发工具编辑模式(现在firefox或chrome都内置了)。

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

我想建议您使用filter_input()原生PHP函数来净化GET和INPUT值。

如果您想进一步提高安全性,对于合理的数据库查询,我建议您使用正则表达式来验证数据格式。 在这种情况下,Preg_match()将帮助您! 但要小心!正则表达式引擎不是那么轻。仅在必要时使用它,否则应用程序性能将下降。

安全是有成本的,但不要浪费你的业绩!

简单的例子:

如果你想再次检查从GET接收到的值是否是一个小于99的数字 如果(! preg_match (' / [0 - 9] {1, 2 }/')){...} 是更重的

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

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

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

准备好的语句/参数化查询足以防止SQL注入,但只有在应用程序中的每个查询都一直使用时才可以。

如果在应用程序的其他地方使用未经检查的动态SQL,它仍然容易受到二阶注入的攻击。

二级注入意味着数据在被包含在查询中之前已经在数据库中循环了一次,并且很难实现。AFAIK,你几乎从未见过真正的工程二级攻击,因为攻击者通常更容易通过社会工程进入,但你有时会因为额外的良性“角色”或类似的东西而出现二级漏洞。

当您可以使一个值存储在数据库中,然后在查询中用作文字时,就可以实现二阶注入攻击。作为一个例子,假设你在一个网站上创建一个帐户时输入了以下信息作为你的新用户名(假设这个问题是MySQL DB):

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

If there are no other restrictions on the username, a prepared statement would still make sure that the above embedded query doesn't execute at the time of insert, and store the value correctly in the database. However, imagine that later the application retrieves your username from the database, and uses string concatenation to include that value a new query. You might get to see someone else's password. Since the first few names in users table tend to be admins, you may have also just given away the farm. (Also note: this is one more reason not to store passwords in plain text!)

We see, then, that if prepared statements are only used for a single query, but neglected for all other queries, this one query is not sufficient to protect against sql injection attacks throughout an entire application, because they lack a mechanism to enforce all access to a database within an application uses safe code. However, used as part of good application design — which may include practices such as code review or static analysis, or use of an ORM, data layer, or service layer that limits dynamic sql — **prepared statements are the primary tool for solving the Sql Injection problem.** If you follow good application design principles, such that your data access is separated from the rest of your program, it becomes easy to enforce or audit that every query correctly uses parameterization. In this case, sql injection (both first and second order) is completely prevented.


* MySql/PHP(很久很久以前)在处理涉及宽字符的参数时是愚蠢的,在这里的另一个高投票的答案中,有一个罕见的情况,可以允许注入通过参数化查询。

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

注意,这只会保护您不受SQL注入的攻击,但您的应用程序仍然容易受到其他类型的攻击。例如,你可以通过使用htmlspecialchars()再次使用正确的编码和引用样式来防止XSS。

简短的回答是肯定的,如果使用得当,PDO准备是足够安全的。


我用这个答案来讨论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));

在某些情况下,它会返回多于一行。让我们来分析一下这里发生了什么:

Selecting a Character Set $pdo->query('SET NAMES gbk'); For this attack to work, we need the encoding that the server's expecting on the connection both to encode ' as in ASCII i.e. 0x27 and to have some character whose final byte is an ASCII \ i.e. 0x5c. As it turns out, there are 5 such encodings supported in MySQL 5.6 by default: big5, cp932, gb2312, gbk and sjis. We'll select gbk here. Now, it's very important to note the use of SET NAMES here. This sets the character set ON THE SERVER. There is another way of doing it, but we'll get there soon enough. The Payload The payload we're going to use for this injection starts with the byte sequence 0xbf27. In gbk, that's an invalid multibyte character; in latin1, it's the string ¿'. Note that in latin1 and gbk, 0x27 on its own is a literal ' character. We have chosen this payload because, if we called addslashes() on it, we'd insert an ASCII \ i.e. 0x5c, before the ' character. So we'd wind up with 0xbf5c27, which in gbk is a two character sequence: 0xbf5c followed by 0x27. Or in other words, a valid character followed by an unescaped '. But we're not using addslashes(). So on to the next step... $stmt->execute() The important thing to realize here is that PDO by default does NOT do true prepared statements. It emulates them (for MySQL). Therefore, PDO internally builds the query string, calling mysql_real_escape_string() (the MySQL C API function) on each bound string value. The C API call to mysql_real_escape_string() differs from addslashes() in that it knows the connection character set. So it can perform the escaping properly for the character set that the server is expecting. However, up to this point, the client thinks that we're still using latin1 for the connection, because we never told it otherwise. We did tell the server we're using gbk, but the client still thinks it's latin1. Therefore the call to mysql_real_escape_string() inserts the backslash, and we have a free hanging ' character in our "escaped" content! In fact, if we were to look at $var in the gbk character set, we'd see: 縗' OR 1=1 /* Which is exactly what the attack requires. The Query This part is just a formality, but here's the rendered query: SELECT * FROM test WHERE name = '縗' OR 1=1 /*' LIMIT 1

恭喜你,你刚刚成功地攻击了一个使用PDO准备语句的程序…

简单的解决方法

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

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

这通常会导致一个真正的预处理语句(即数据从查询中以单独的数据包发送)。但是,请注意PDO将会无声地退回到模拟MySQL不能在本地准备的语句:手册中列出了那些它可以准备的语句,但要注意选择适当的服务器版本)。

正确的解决方法

这里的问题是我们使用了SET NAMES而不是C API的mysql_set_charset()。否则,攻击不会成功。但最糟糕的是,PDO直到5.3.6才公开mysql_set_charset()的C API,所以在以前的版本中,它不能防止每个可能的命令都受到这种攻击! 它现在被暴露为一个DSN参数,应该使用它而不是SET NAMES…

前提是我们使用的是2006年以后的MySQL版本。如果您使用的是较早的MySQL版本,那么mysql_real_escape_string()中的一个错误意味着无效的多字节字符(例如我们的有效负载中的那些字符)被当作单字节进行转义,即使客户端已经正确地通知了连接编码,因此这种攻击仍然会成功。该错误已在MySQL 4.1.20, 5.0.22和5.1.11中修复。

可取之处

正如我们在开始时所说,要使这种攻击起作用,必须使用易受攻击的字符集对数据库连接进行编码。utf8mb4不容易受到攻击,但可以支持所有Unicode字符:所以你可以选择使用它,但它只在MySQL 5.5.3之后可用。另一种替代方法是utf8,它也不容易受到攻击,并且可以支持整个Unicode基本多语言平面。

Alternatively, you can enable the NO_BACKSLASH_ESCAPES SQL mode, which (amongst other things) alters the operation of mysql_real_escape_string(). With this mode enabled, 0x27 will be replaced with 0x2727 rather than 0x5c27 and thus the escaping process cannot create valid characters in any of the vulnerable encodings where they did not exist previously (i.e. 0xbf27 is still 0xbf27 etc.)—so the server will still reject the string as invalid. However, see @eggyal's answer for a different vulnerability that can arise from using this SQL mode (albeit not with 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)

OR

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

OR

开启NO_BACKSLASH_ESCAPES SQL模式

你100%安全。

否则,即使您使用PDO预处理语句,您也很容易受到攻击…

齿顶高

我一直在慢慢地编写一个补丁,将默认值更改为不模拟,为PHP的未来版本做准备。我遇到的问题是,当我这样做时,很多测试都中断了。一个问题是模拟的准备只会在执行时抛出语法错误,但真正的准备会在准备时抛出错误。所以这可能会导致问题(也是测试令人厌烦的部分原因)。