PHP防注入总结

Posted by jintang on 2013-12-22

PHP接受外部数据的规则:

规则1:绝不要信任外部数据或输入

有潜在问题的代码:

1
2
3
4
5
<?php

$username = $_POST['username']; // 危险!
$userList = array($username, "jay", "ken"); // 危险!
define('CURRENT_USRE', $username); // 危险!

可以看出$username是来自表单提交$_POST二来,用户可以在这个输入域中输入任何字符串,包括用来清除文件或运行以前上传的文件的恶意命令。您可能会问,“难道不能使用只接受字母 A-Z 的客户端(Javascrīpt)表单检验脚本来避免这种危险吗?”。
但是不要相信任何外部数据。

解决这个问题很简单,就是对$_POST[‘username’]进行清理。
假设我们只希望$_POST[‘username’]获得[a-z]范围的字符串

1
2
3
4
5
6
7
8
9
10
11
12
<?php
$username = cleanInput($_POST['username']); // clean!!
$userList = array($username, "jay", "ken"); // clean!!
define('CURRENT_USRE', $username); // clean!!

function cleanInput($str)
{
$clean = strtolower($str);
$clean = preg_replace("/[^a-z]/", "", $clean);
$clean = substr($clean,0,12);
return $clean;
}

规则2:禁用那些使安全性难以实施的PHP配置

比如:

  1. 确保禁用register_globals
  2. 确保禁用系统命令

end so on…

规则3:如果不能理解它,就不能保护它

有些代码写法可能比较简洁,但是语义模糊,或者不够清晰,就很容易出现漏洞

语义不清晰的代码

1
2
<?php
$input = isset($_POST['username']) ? $_POST['username'] : "";

语义更好的代码:

1
2
3
4
5
<?php
$input = "";
if (isset($_POST['username'])) {
$input = $_POST['username'];
}

规则4:纵深防御

同时在处理表单的 PHP 代码中采用必要的措施。同样,即使使用 PHP regex 来确保 GET 变量完全是数字的,仍然可以采取措施确保 SQL 查询使用转义的用户输入。
这么一连串的措施就是纵深防御的思想

SQL防注入

常见的SQL注入漏洞就是$_GET[‘id’]获取用户id

1
2
3
4
<?php
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = '$id'";
// execute

如果$_GET[‘id’]得到的是一个字符串:’ or 1 = 1’,将会得到这样的sql

1
SELECT * FROM users WHERE id = '' or 1 = 1

这样全部user都被查出来了

解决办法

  1. 使用mysql_real_escape_string来转义
1
2
3
4
<?php
$id = $_GET['id'];
$sql = "SELECT * FROM users WHERE id = '" . mysql_real_escape_string($id) . "'";
// execute

第三个mysql_real_escape_string之所以能够防注入是因为mysql_escape_string本身并没办法 判断当前的编码,必须同时指定服务端的编码和客户端的编码,加上就能防编码问题的注入了。
虽然是可以一定程度上防止SQL注入,但是有还是被黑客绕过的风向。

  1. Prepare Statement机制

解决方案就是使用拥有Prepared Statement机制的PDO和MYSQLi来代替mysql_query(注:mysql_query自 PHP 5.5.0 起已废弃,并在将来会被移除):

PDO:

1
2
3
4
5
6
7
8
9
10
11
<?php
$name = $POST['username'];
$pdo = new PDO('mysql:dbname=dbtest;host=127.0.0.1;charset=utf8', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$stmt = $pdo->prepare('SELECT * FROM employees WHERE name = :name');
$stmt->bindParam(1, $name, PDO::PARAM_STR); // 进行类型绑定
$stmt->execute();
foreach ($stmt as $row) {
// do something with $row
}

Mysqli:

1
2
3
4
5
6
7
8
9
<?php
$name = $POST['username'];
$stmt = $dbConnection->prepare('SELECT * FROM employees WHERE name = ?');
$stmt->bind_param('s', $name); // 进行类型绑定
$stmt->execute();
$result = $stmt->get_result();
while ($row = $result->fetch_assoc()) {
// do something with $row
}

$_GET变量攻击

如果想要获取用户ID:$_GET[‘id’],我们只希望id是一个[0-9]的数字。

  1. 使用is_numeric()
1
2
3
4
5
6
7
<?php
$id = 0;
if (is_numeric($_GET['id'])) {
$id = $_GET['id'];
} else {
// throw exception
}

虽然有效限定了数字,但是一下几个值仍然有效:

1
2
3
4
100 (有效) 
100.1 (不应该有小数位)
+0123.45e6 (科学计数法 —— 不好)
0xff33669f (十六进制 —— 危险!危险!)

更高效的方式就是使用正则匹配:

1
2
3
4
5
<?php
$id = $_GET['id'];
if (!preg_match("/^[0-9]+$/",$id)){
// throw exception
}

仅仅这样还不够,如果$_GET[‘id’]给你一个1000000000000000,超大的数字字符串,能够满足你的正则匹配,但是仍然有可能使你的代码可能产生溢出。
所以如果你的id最大是99999,那么你可以”纵深防御”,再加一个判断

1
2
3
4
5
<?php
$id = $_GET['id'];
if (!preg_match("/^[0-9]+$/",$id) && strlen($id) > 5){
// throw exception
}

strlen判断id是否超过5位数

跨站点脚本攻击

在跨站点脚本(XSS)攻击中,往往有一个恶意用户在表单中(或通过其他用户输入方式)输入信息,这些输入将恶 意的客户端标记插入过程或数据库中。例如,假设站点上有一个简单的来客登记簿程序,让访问者能够留下姓名、电子邮件地址和简短的消息。恶意用户可以利用这 个机会插入简短消息之外的东西,比如对于其他用户不合适的图片或将用户重定向到另一个站点的 Javascrīpt,或者窃取 cookie 信息。

幸运的是,PHP 提供了 strip_tags() 函数,这个函数可以清除任何包围在 HTML 标记中的内容

1
2
<?php
$content = strip_tags($_POST['content']);

远程表单提交

首先可能考虑检查 $_SERVER[’HTTP_REFERER’],从而判断请求是否来自自己的服务器,这种方法可以挡住大多数恶意用户,但是挡不住最高明的黑客。这些人足够聪明,能够篡改头部中的引用者信息,使表单的远程副本看起来像是从您的服务器提交的。
处理远程表单提交更好的方式是,根据一个惟一的字符串或时间戳生成一个令牌,并将这个令牌放在会话变量和表单中。提交表单之后,检查两个令牌是否匹配。如果不匹配,就知道有人试图从表单的远程副本发送数据。