SQL注入是一个控制数据库查询的技术,往往会导致丧失机密性。在某些情况下,如果成功执行SELECT 'phpeval(base64_decode("someBase64EncodedDataHere"));' INTO OUTFILE '/var/www/reverse_shell.php'
将导致服务器被攻击者拿下,而代码注入(包括SQL,LDAP,操作系统命令,XPath注入技术)长年保持在OWASP漏洞排名前十。
更多人分享有关于应用安全的知识是一件极好的事。然而不幸的是,网络上流传的大部分东西(尤其是老博客文章,高搜索引擎排名)都已经过时了。虽然是无意的误导,但是却造成了很大的威胁。
如何防止SQL注入
使用预处理语句,也被称为参数化查询。例如:$stmt = $pdo->prepare('SELECT * FROM blog_posts WHERE YEAR(created) = ? AND MONTH(created) = ?');if (!$stmt->execute([$_GET['year'], $_GET['month']])) {
header("Location: /blog/"); exit;
}
$posts = $stmt->fetchAll(\PDO::FETCH_ASSOC);
在PHP应用中预处理语句过滤掉任何SQL注入的可能,无论是什么都需要先传递到$_GET变量。SQL查询语句是攻击者无法改变的(除非你将PDO::ATTR_EMULATE_PREPARES 开启了,这也意味着你还没有真正使用预处理语句)
预处理语句解决应用安全的根本问题:通过发送完全独立的包将操作指令与数据进行单独处理。这和导致堆栈溢出的问题有点类似了。
只要你没有用SQL语句连接user-provided变量和环境变量(并且保证你没有使用emulated prepares),那你就不必担心交叉SQL注入的问题了。
重要说明
预处理语句确保WEB应用与数据库服务之间的交互(即使两者不在同一台机器上,也会通过TLS进行连接)。攻击者还有可能在字段中存储一个payload,这是相当危险的,比如一个存储过程,我们称之为高阶SQL注入。
在这种情况下,我们建议不要编写存储过程,它会制造一个高阶SQL注入点。
关于输出过滤
应该有人看到过上面这张关于SQL注入攻击的漫画吧,在一些安全会议上甚至都经常被拿来引用,尤其是写给新人的文章中。这张漫画提醒了我们要提高对数据库查询中危险用户输入的意识,但是漫画中的建议却是过滤掉数据库输入,通过对相关问题的理解,我们知道这仅仅是一个折衷的办法。
最好忘记过滤输入
虽然可以在数据发送到数据库之前重写传入的数据流来防止攻击者的攻击,但是这个过程比较难以把控。
除非你想花时间去研究,达到完全掌握所有Unicode格式应用程序,你最好不要尝试过滤你的输入。
此外,改变你的输入数据流可能造成数据损坏。特别是在你正在处理原始二进制文件(图片,加密信息)的时候。
预处理语句能简单粗暴的防止SQL注入
XKCD作者Randall Munroe是个聪明人。如果这幅漫画直到今天才被创作出来,大概会是这个样子的
输入仍应进行验证
数据验证与过滤完全不是一回事,预处理语句可以防止SQL注入,然而并不能让你摆脱那些糟糕的数据。大多数情况下,会使用filter_var()$email = filter_var($_POST['email'], FILTER_VALIDATE_EMAIL);
if (empty($email)) {
throw new \InvalidArgumentException('Invalid email address');
}
列和表标识符
当列和表标识符作为查询语句中的一部分,你不能使用参数表示它们。如果你正在开发的这个应用需要一个动态查询结构,请使用白名单。
白名单是一个应用程序逻辑策略,它只允许少数可信的值。相对来说黑名单,仅仅是禁止已知的恶意输入。
大多数情况下,使用白名单比黑名单更安全!$qs = 'SELECT * FROM photos WHERE album = ?';
// Use switch-case for an explicit whitelist
switch ($_POST['orderby']) {
case 'name':
case 'exifdate':
case 'uploaded':
// These strings are trusted and expected
$qs .= ' ORDER BY '.$_POST['orderby'];
if (!empty($_POST['asc'])) {
$qs .= ' ASC';
} else {
$qs .= ' DESC';
}
default:
// Some other value was passed. Let's just order by photo ID in descending order.
$qs .= ' ORDER BY photoid DESC';
}$stmt = $db->prepare($qs)
;if ($stmt->execute([$_POST['album_id']])) {
$photos = $stmt->fetchAll(\PDO::FETCH_ASSOC);
}
使用预处理语句看起来很麻烦?
开发者第一次遇到预处理语句,对于需要写大量的冗余代码而感到沮丧(提取,执行,取回;提取,执行,取回;….令人厌烦)
由此,EasyDB[https://github.com/paragonie/easydb]诞生了。
如何使用EasyDB
这里有两种方法。你可以使用EasyDB包含你的PDO
如果你熟悉PDO构造,你可以使用\ParagonIE\EasyDB\Factory::create()参数来进行替代// First method:
$pdo = new \PDO('mysql;host=localhost;dbname=something', 'username', 'putastrongpasswordhere');
$db = \ParagonIE\EasyDB\EasyDB($pdo, 'mysql');
// Second method:
$db = \ParagonIE\EasyDB\Factory::create('mysql;host=localhost;dbname=something', 'username', 'putastrongpasswordhere');
如果有一个EasyDB对象,你可以开始利用它的快速开发安全数据库应用程序。
预处理语句:安全数据库查询$data = $db->safeQuery(
'SELECT * FROM transactions WHERE type = ? AND amount >= ? AND date >= ?',
[
$_POST['ttype'],
$_POST['minimum'],
$_POST['since']
]
);
从一个数据库表中选择多行$rows = $db->run(
'SELECT * FROM comments WHERE blogpostid = ? ORDER BY created ASC',
$_GET['blogpostid']
);
foreach ($rows as $row) {
$template_engine->render('comment', $row);
}
从数据库表中选择一行$userData = $db->row(
"SELECT * FROM users WHERE userid = ?",
$_GET['userid']
);
向数据库表中插入新的一行$db->insert('comments', [
'blogpostid' => $_POST['blogpost'],
'userid' => $_SESSION['user'],
'comment' => $_POST['body'],
'parent' => isset($_POST['replyTo']) ? $_POST['replyTo'] : null
]);
动态查询中躲避标识符(列/表/视图名)$qs = 'SELECT * FROM some_table';
$and = false;
if (!empty($where)) {
$qs .= ' WHERE ';
foreach (\array_keys($where) as $column) {
if (!\in_array($column, $whiteListOfColumnNames)) {
continue;
}
if ($and) {
$qs .= ' AND ';
}
$qs .= $db->escapeIdentifier($column).' = ?';
$and = true;
}
}$qs .= ' ORDER BY rowid DESC';
// And then to fetch some data
$data = $db->run($qs, \array_values($where);
警告:escapeIdentifier() 方法意味着输入不应该被转义。
安全从开发人员开始!