PHP安全配置
一、CGI模式安装安全
二、以Apache模块安装安全
当 PHP 以 Apache 模块方式安装时,它将继承 Apache 用户(通常为“nobody”)的权限。这对安全和认证有一些影响。比如,如果用 PHP 来访问数据库,除非数据库有自己的访问控制,否则就要使“nobody”用户可以访问数据库。这意味着恶意的脚本在不用提供用户名和密码时就可能访问和修改数据库。一个 web Spider 也完全有可能偶然发现数据库的管理页面,并且删除所有的数据库。可以通过 Apache 认证来避免此问题,或者用 LDAP、.htaccess 等技术来设计自己的防问模型,并把这些代码作为 PHP 脚本的一部份
一个常犯的对安全性不利的错误就是让 Apache 拥有 root 权限,或者通过其它途径斌予 Apache 更强大的功能。
把 Apache 用户的权限提升为 root 是极度危险的做法,而且可能会危及到整个系统的安全。所以除非是安全专家,否则决不要考虑使用 su,chroot 或者以 root 权限运行。
除此之外还有一些比较简单的解决方案。比如说可以使用 open_basedir 来限制哪些目录可以被 PHP 使用。也可以设置 Apache 的专属区域,从而把所有的 web 活动都限制到非用户和非系统文件之中。
三、文件系统安全
PHP 遵从大多数服务器系统中关于文件和目录权限的安全机制。这就使管理员可以控制哪些文件在文件系统内是可读的。必须特别注意的是全局的可读文件,并确保每一个有权限的用户对这些文件的读取动作都是安全的。
PHP 被设计为以用户级别来访问文件系统,所以完全有可能通过编写一段 PHP 代码来读取系统文件如 /etc/passwd,更改网络连接以及发送大量打印任务等等。因此必须确保 PHP 代码读取和写入的是合适的文件。
两个重要措施来防止此类问题。
# 只给 PHP 的 web 用户很有限的权限。
# 检查所有提交上来的变量。
$username = $_SERVER['REMOTE_USER']; // 使用认证机制 $userfile = $_POST['user_submitted_filename']; $homedir = "/home/$username"; $filepath = "$homedir/$userfile"; if (!ctype_alnum($username) || !preg_match('/^("Bad username/filename"); }
* Null字符问题
由于 PHP 的文件系统操作是基于 C 语言的函数的,所以它可能会以您意想不到的方式处理 Null 字符。 Null字符在 C 语言中用于标识字符串结束,一个完整的字符串是从其开头到遇见 Null 字符为止。
# 会被 Null 字符问题攻击的代码:
$file = $_GET['file']; // "http://www.cnblogs.com/etc/passwd\0" if (file_exists('/home/wwwrun/'.$file.'.php')) { // file_exists will return true as the file /home/wwwrun/http://www.cnblogs.com/etc/passwd exists include '/home/wwwrun/'.$file.'.php'; // the file /etc/passwd will be included }
# 验证输入的正确做法
$file = $_GET['file']; // 对字符串进行白名单检查 switch ($file) { case 'main': case 'foo': case 'bar': include '/home/wwwrun/include/'.$file.'.php'; break; default: include '/home/wwwrun/include/main.php'; }
四、数据库安全
* 设计数据库
第一步一般都是创建数据库,除非是使用第三方的数据库服务。当创建一个数据库的时候,会指定一个所有者来执行和新建语句。通常,只有所有者(或超级用户)才有权对数据库中的对象进行任意操作。如果想让其他用户使用,就必须赋予他们权限。
应用程序永远不要使用数据库所有者或超级用户帐号来连接数据库,因为这些帐号可以执行任意的操作,比如说修改数据库结构(例如删除一个表)或者清空整个数据库的内容。
应该为程序的每个方面创建不同的数据库帐号,并赋予对数据库对象的极有限的权限。仅分配给能完成其功能所需的权限,避免同一个用户可以完成另一个用户的事情。这样即使攻击者利用程序漏洞取得了数据库的访问权限,也最多只能做到和该程序一样的影响范围。
鼓励用户不要把所有的事务逻辑都用 web 应用程序(即用户的脚本)来实现。最好用视图(view)、触发器(trigger)或者规则(rule)在数据库层面完成。当系统升级的时候,需要为数据库开辟新的接口,这时就必须重做所有的数据库客户端。除此之外,触发器还可以透明和自动地处理字段,并在调试程序和跟踪事实时提供有用的信息。
* 连接数据库
把连接建立在 SSL 加密技术上可以增加客户端和服务器端通信的安全性,或者 SSH 也可以用于加密客户端和数据库之间的连接。如果使用了这些技术的话,攻击者要监视服务器的通信或者得到数据库的信息是很困难的。
* 加密存储模型
SSL/SSH 能保护客户端和服务器端交换的数据,但 SSL/SSH 并不能保护数据库中已有的数据。SSL 只是一个加密网络数据流的协议。
如果攻击者取得了直接访问数据库的许可(绕过 web 服务器),敏感数据就可能暴露或者被滥用,除非数据库自己保护了这些信息。对数据库内的数据加密是减少这类风险的有效途径,但是只有很少的数据库提供这些加密功能。
对于这个问题,有一个简单的解决办法,就是创建自己的加密机制,然后把它用在 PHP 程序内。PHP 有几个扩展库可以完成这个工作,比如说 Mcrypt 和 Mhash 等,它们包含多种加密运算法则。脚本在插入数据库之前先把数据加密,以后提取出来时再解密。
对某些真正隐蔽的数据,如果不需要以明文的形式存在(即不用显示),可以考虑用散列算法。使用散列算法最常见的例子就是把密码经过 MD5 加密后的散列存进数据库来代替原来的明文密码。参见 crypt() 和 md5()。
* SQL注入
很多 web 开发者没有注意到 SQL 查询是可以被篡改的,因而把 SQL 查询当作可信任的命令。殊不知道,SQL 查询可以绕开访问控制,从而绕过身份验证和权限检查。更有甚者,有可能通过 SQL 查询去运行主机操作系统级的命令。
直接 SQL 命令注入就是攻击者常用的一种创建或修改已有 SQL 语句的技术,从而达到取得隐藏数据,或覆盖关键的值,甚至执行数据库主机操作系统命令的目的。这是通过应用程序取得用户输入并与静态参数组合成 SQL 查询来实现的。下面将会给出一些真实的例子。
由于在缺乏对输入的数据进行验证,并且使用了超级用户或其它有权创建新用户的数据库帐号来连接,攻击者可以在数据库中新建一个超级用户。
Example #1 一段实现数据分页显示的代码……也可以被用作创建一个超级用户(PostgreSQL系统)。
<"SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;"; $result = pg_query($conn, $query); "codetitle">复制代码 代码如下:
<"SELECT id, name, inserted, size FROM products
WHERE size = '$size'
ORDER BY $order LIMIT $limit, $offset;";
$result = odbc_exec($conn, $query);
"codetitle">复制代码 代码如下:
<"UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
"hehehe', admin='yes', trusted=100 "(后面有个空格)去获得更多的权限。这样做的话,查询语句实际上就变成了:复制代码 代码如下:
<"UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%'; --";// $pwd == "hehehe', admin='yes', trusted=100 "
$query = "UPDATE usertable SET pwd='hehehe', admin='yes', trusted=100 WHERE
...;";
"codetitle">复制代码 代码如下:
<"SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);
"codetitle">复制代码 代码如下:
<"SELECT * FROM products
WHERE id LIKE '%a%'
exec master..xp_cmdshell 'net user test testpass /ADD'--";
$result = mssql_query($query);
"codetitle">复制代码 代码如下:
<"SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";// 请注意格式字符串中的 %d,如果用 %s 就毫无意义了
$query = sprintf("SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;",
$offset);
"codetitle">复制代码 代码如下:
<form method="post" action="attacktarget">
<input type="hidden" name="username" value="badfoo" />
<input type="hidden" name="password" value="badfoo" />
</form>
通常 PHP 所返回的错误提示都能帮助开发者调试程序,它会提出哪个文件的哪些函数或代码出错,并指出错误发生的在文件的第几行,这些就是 PHP 本身所能给出的信息。很多 PHP 开发者会使用 show_source()、highlight_string()或者 highlight_file()函数来调试代码,但是在正式运行的网站中,这种做法可能会暴露出隐藏的变量、未检查的语法和其它的可能危及系统安全的信息。在运行一些具有内部调试处理的程序,或者使用通用调试技术是很危险的。如果让攻击者确定了程序是使用了哪种具体的调试技术,他们会尝试发送变量来打开调试功能:
Example #2 利用变量打开调式功能
复制代码 代码如下:
<form method="post" action="attacktarget">
<input type="hidden" name="errors" value="Y" />
<input type="hidden" name="showerrors" value="1" />
<input type="hidden" name="debug" value="1" />
</form>
不管错误处理机制如何,可以探测系统错误的能力会给攻击者提供更多信息。
比如说,PHP 的独有的错误提示风格可以说明系统在运行 PHP。如果攻击者在寻找一个 .html 为页面,想知道其后台的技术(为了寻找系统弱点),他们就会把错误的数据提交上去,然后就有可以得知系统是基于 PHP 的了。
一个函数错误就可能暴露系统正在使用的数据库,或者为攻击者提供有关网页、程序或设计方面的有用信息。攻击者往往会顺藤摸瓜地找到开放的数据库端口,以及页面上某些 bug 或弱点等。比如说,攻击者可以一些不正常的数据使程序出错,来探测脚本中认证的顺序(通过错误提示的行号数字)以及脚本中其它位置可能泄露的信息。
一个文件系统或者 PHP 的错误就会暴露 web 服务器具有什么权限,以及文件在服务器上的组织结构。开发者自己写的错误代码会加剧此问题,导致泄漏了原本隐藏的信息。
有三个常用的办法处理这些问题。第一个是彻底地检查所有函数,并尝试弥补大多数错误。第二个是对在线系统彻底关闭错误报告。第三个是使用 PHP 自定义的错误处理函数创建自己的错误处理机制。根据不同的安全策略,三种方法可能都适用。
一个能提前阻止这个问题发生的方法就是利用 error_reporting() 来帮助使代码更安全并发现变量使用的危险之处。在发布程序之前,先打开 E_ALL 测试代码,可以帮你很快找到变量使用不当的地方。一旦准备正式发布,就应该把 error_reporting() 的参数设为 0 来彻底关闭错误报告或者把 php.ini中的 display_errors 设为 off 来关闭所有的错误显示以将代码隔绝于探测。当然,如果要迟一些再这样做,就不要忘记打开 ini 文件内的 log_errors 选项,并通过 error_log 指定用于记录错误信息的文件。Example #3 用 E_ALL 来查找危险的变量
复制代码 代码如下:
<"/highly/sensitive/data/index.html");
}
"codetitle">复制代码 代码如下:
<"/highly/sensitive/data.php";
}
"codetitle">复制代码 代码如下:
<"Hello <b>{$_SESSION['username']}</b>";} else {
echo "Hello <b>Guest</b><br />";
echo "Would you like to login";}
"codetitle">复制代码 代码如下:
<"admin@example.com", "Possible breakin attempt", $_SERVER['REMOTE_ADDR']);
echo "Security violation, admin has been alerted.";
exit;} else {
// 这一次请求中并没有设置 MAGIC_COOKIE 变量
}
"codetitle">复制代码 代码如下:
<"(双引号),\(反斜线)和 NULL 字符都会被自动加上一个反斜线进行转义。这和 addslashes() 作用完全相同。一共有三个魔术引号指令:
■magic_quotes_gpc 影响到 HTTP 请求数据(GET,POST 和 COOKIE)。不能在运行时改变。在 PHP 中默认值为 on。 参见 get_magic_quotes_gpc()。
■magic_quotes_runtime 如果打开的话,大部份从外部来源取得数据并返回的函数,包括从数据库和文本文件,所返回的数据都会被反斜线转义。该选项可在运行的时改变,在 PHP 中的默认值为 off。 参见 set_magic_quotes_runtime() 和 get_magic_quotes_runtime()。
■magic_quotes_sybase 如果打开的话,将会使用单引号对单引号进行转义而非反斜线。此选项会完全覆盖 magic_quotes_gpc。如果同时打开两个选项的话,单引号将会被转义成 ''。而双引号、反斜线 和 NULL 字符将不会进行转义。 如何取得其值参见 ini_get()。
* 魔术引号的作用优点:
■ 对初学者很有用 魔术引号在 PHP 中用来实现避免初学者的代码更危险。尽管 SQL 注入在魔术引号打开的情况下仍然有可能实现,但起码系统的风险减少很多了。
■ 方便使用 当向数据库中插入数据时,魔术引号所做的就是自动对所有的 GET、POST、COOKIE 数据运用 addslashes() 函数缺点:
■可移植性 编程时认为其打开或并闭都会影响到移植性。可以用 get_magic_quotes_gpc() 来检查是否打开,并据此编程。
■ 性能 由于并不是每一段被转义的数据都要插入数据库的,如果所有进入 PHP 的数据都被转义的话,那么会对程序的执行效率产生一定的影响。在运行时调用转义函数(如 addslashes())更有效率。 尽管 php.ini-dist 默认打开了这个选项,但是 php.ini-recommended 默认却关闭了它,主要是出于性能的考虑。
■ 不便 由于不是所有数据都需要转义,在不需要转义的地方看到转义的数据就很烦。比如说通过表单发送邮件,结果看到一大堆的 \'。针对这个问题,可以使用 stripslashes() 函数处理
* 关闭魔术引号
magic_quotes_gpc指令只能在系统级关闭,不能在运行时。也就是说不能用 ini_set()。
Example #1 在服务器端关闭魔术引号下面是一个通过 php.ini 文件把这些选项设为 Off的范例。更多信息请参见本手册的怎样修改配置设定。
; Magic quotes
;; Magic quotes for incoming GET/POST/Cookie data.
magic_quotes_gpc = Off; Magic quotes for runtime-generated data, e.g. data from SQL, from exec(), etc.
magic_quotes_runtime = Off; Use Sybase-style magic quotes (escape ' with '' instead of \').
magic_quotes_sybase = Off
如果不能修改服务器端的配置文件,使用 .htaccess 也可以。范例如下:php_flag magic_quotes_gpc Off
为了能写出移植性较强的代码(可以运行于任何环境),例如不能修改服务器配置的情况,下面的例子可以在运行时关闭 magic_quotes_gpc。但是这样做比较低效,适当的修改配置才是更好的办法。
Example #2 在运行时关闭魔术引号
复制代码 代码如下:
<?php
if (get_magic_quotes_gpc()) {
function stripslashes_deep($value)
{
$value = is_array($value) ?
array_map('stripslashes_deep', $value) :
stripslashes($value);return $value;
}$_POST = array_map('stripslashes_deep', $_POST);
$_GET = array_map('stripslashes_deep', $_GET);
$_COOKIE = array_map('stripslashes_deep', $_COOKIE);
$_REQUEST = array_map('stripslashes_deep', $_REQUEST);
}
?>
九、隐藏PHP
一般而言,通过隐藏的手段提高安全性被认为是作用不大的做法。但某些情况下,尽可能的多增加一份安全性都是值得的。
一些简单的方法可以帮助隐藏 PHP,这样做可以提高攻击者发现系统弱点的难度。在 php.ini 文件里设置 expose_php = off ,可以减少他们能获得的有用信息。
另一个策略就是让 web 服务器用 PHP 解析不同扩展名。无论是通过 .htaccess 文件还是 Apache 的配置文件,都可以设置能误导攻击者的文件扩展名:
Example #1 把 PHP 隐藏为另一种语言
# 使PHP看上去像其它的编程语言
AddType application/x-httpd-php .asp .py .pl或者干脆彻底隐藏它:Example #2 使用未知的扩展名作为 PHP 的扩展名
# 使 PHP 看上去像未知的文件类型
AddType application/x-httpd-php .bop .foo .133t或者把它隐藏为 HTML 页面,这样所有的 HTML 文件都会通过 PHP 引擎,会为服务器增加一些负担:Example #3 用 HTML 做 PHP 的文件后缀
# 使 PHP 代码看上去像 HTML 页面
AddType application/x-httpd-php .htm .html要让此方法生效,必须把 PHP 文件的扩展名改为以上的扩展名。这样就通过隐藏来提高了安全性,虽然防御能力很低而且有些缺点。十、保持更新
PHP 和其它的大型系统一样,在持续的研究和改进中。每一个版本都会有或多或少的改进来增强安全性和修复任何缺陷,配置问题以及任何会影响到整个系统安全与性能的问题。
和其它系统级的脚本语言一样,最好的途径是经常更新,并时刻留意最新版本及其改变
下一篇:用PHP 4.2书写安全的脚本