30.4. 全局会话管

会话的默认行为可以由Zend_Session中的一些静态方法来改变。使用Zend_Session来处理和操作所有的全局会话管理,包括使用Zend_Session_Core::setOptions()方法对PHP内置的会话模块提供的常用配置选项的配置。例如,不能确保使用安全的save_path和PHP会话模块使用的唯一的cookie名,那么使用Zend_Session::setOptions()会引发安全问题。

30.4.1. Zend_Session::setOptions()

当第一个会话命名空间被请求时,Zend_Session就会被自动调用,除非已经明确调用过Zend_Session::start()了。内部PHP的会话将会按照Zend_Session的默认配置开启,除非先前调用过Zend_Session::setOptions()修改了配置。

传递给Zend_Session::setOptions()方法的参数是基本名(也就是不包含"session.前缀")及其值组成的数组。要是没有设置过选项,Zend_Session首先会利用推荐的选项设置,也就是php.ini中的默认设置。有关选项设置的最佳实践,可以反馈到:fw-auth@lists.zend.com

可以使用Zend_Config_Ini组件的".ini"配置文件来自动地配置Zend_Session组件:

例 30.16. 使用Zend_Config组件配置Zend_Session组件

<?php
$config = new Zend_Config_Ini('myapp.ini', 'sandbox');
require_once 'Zend/Session.php';
Zend_Session::setOptions($config->asArray()); 
?>

对应"myapp.ini"的文件内容:

例 30.17. myapp.ini


;  Defaults for our live servers
[live]
; bug_compat_42
; bug_compat_warn
; cache_expire
; cache_limiter
; cookie_domain
; cookie_lifetime
; cookie_path
; cookie_secure
; entropy_file
; entropy_length
; gc_divisor
; gc_maxlifetime
; gc_probability
; hash_bits_per_character
; hash_function
; name should be unique for each PHP application sharing the same domain name
name = UNIQUE_NAME
; referer_check
; save_handler
; save_path
; serialize_handler
; use_cookies
; use_only_cookies
; use_trans_sid

; remember_me_seconds = <integer seconds>
; strict = on|off


; Our sandbox server uses the same settings as our live server,
; except as overridden below:
[sandbox : live]
; Don't forget to create this directory and make it rwx (readable and modifiable) by PHP.
save_path = /home/myaccount/zend_sessions/myapp
use_only_cookies = on
; When persisting session id cookies, request a TTL of 10 days
remember_me_seconds = 864000

30.4.2. 选项

上述大多数选项没有做解释,因为可以在PHP的官方文档中找到他们的解释。

  • boolean strict - 开启了该选项,构造Zend_Session_Namespace实例时不自动去调用Zend_Session开启会话。

  • integer remember_me_seconds - 该选项指明了当用户代理结束(比如浏览器窗口关闭)后,会话标识符还将保存在cookie中的时长。

  • string save_path - 该值跟系统相关,开发者需提供一个PHP程序有读写权限的目录的绝对路径

    [注意] 安全风险

    如果其他应用程序有读这个目录路径的权限,那么就有发生会话劫持的可能性。如果其他应用程序有写这个目录路径的权限,那么就有发生会话污染的可能性。如果这个目录路径是与其他用户或PHP应用程序共享的,那么会引起大量的安全问题,包括会话数据盗窃,会话劫持,垃圾回收冲突(举例来说,另一个用户的PHP应用程序可能会删除你的应用程序的会话文件)。

    例如,攻击者可以访问受害者的站点,获得会话cookie。在访问攻击者的站点执行var_dump($_SESSION)之前,编辑cookie路径为在相同服务器上的他自己的域名。知道了有关受害者会话的详细信息,攻击者就可以修改受害者的会话状态(也就是会话中毒),把会话路径改回受害者的站点,然后使得来自受害者站点的请求使用已被污染了的会话。同个服务器上的两个应用程序不能读写对方应用程序的save_path,但如果save_path是可以猜测的,且攻击者拥有这2个站点其中一个的控制权,攻击者就可以修改他的站点会话的save_path为另一个站点的会话的save_path,从而就造成了会话污染。因此,save_path的值不能公开,且对每个应用程序必须是唯一的、安全的。

  • string name - 该值跟系统相关,开发者需为基于ZF的应用程序提供一个简短且唯一的值。

    [注意] 安全风险

    如果php.inisession.name的值是相同的(默认为“PHPSESSID”),且在同个域名下面(比如http://www.somewebhost.com/~youraccount/index.php)有2个及以上的PHP应用程序,那么当访问者访问这些站点时它们共享了会话数据。此外,很可能引起会话数据的腐败。

  • boolean use_only_cookies - 为了不引入更多的安全风险,不要修改该选项的默认值。

    [注意] 安全风险

    如果该选项没有被激活,攻击者使用攻击者站点上的链接,可以轻松的固定受害人的会话标识符,比如:http://www.victim -website.com/index.php?PHPSESSID=fixed_session_id。假使受害者还没有一个vicitim- website.com站点会话标识符的cookie,那么会话固定就成功了。一旦受害者使用了攻击者指定的会话标识符,那么攻击者就能劫持受害者的会话,并模仿受害者的用户代理,试图假装成受害者。

30.4.3. regenerateId()

30.4.3.1. 简介: 会话标识符

简介:在基于ZF的应用程序中有关会话使用的问题,提倡使用浏览器的cookie是最佳的实践,而不是把会话的标识符跟在URL后面的方式来追踪用户。Zend_Session组件默认的只有cookie才能保持会话标识符。cookie的值是浏览器会话的唯一标识符。PHP内置的会话模块使用这个标识符以保持站点访问者与每个访问者的持久会话数据之间一对一的关系。Zend_Session组件包装了会话存储器($_SESSION)并提供了一个面向对象的接口。不幸的是,如果攻击者能访问受害者的cookie值(会话标识符),攻击者就能劫持受害者的会话。这个问题不仅在PHP中存在,在Zend Framework中也存在。regenerateId()方法能使应用程序重新生成会话标识符(储存在访问者的cookie中),标识符为一个随机的、不可预计的值。注意:虽然“用户代理(user agent)”和“Web浏览器(web browser)”不相同,为了使得本章节更易读,我们使用的这两个术语可以互换。

为什么?:如果攻击者获得了受害者有效的会话标识符,攻击者就可能假扮成一个有效的用户(受害者),得到了访问机密信息或者操作受害者在你的应用程序中的数据。更新会话标识符有利于阻碍会话劫持的发生。如果会话标识符改变了,攻击者就不知道新的会话标识,也就不能用新的会话标识劫持受害者的会话了。即使攻击者能够访问旧的会话标识,regenerateId()将会话数据从旧的标识符下移到了新的标识符下,所以通过旧的会话标识符访问不到会话数据。

何时使用regenerateId():在你的Zend框架程序引导文件中添加Zend_Session::regenerateId (),以最安全的方式重新生成用户Web浏览器cookie中的会话标识符。如果不需要有条件的判定何时重新生成会话标识符,那么这样的方式就没什么缺陷。虽然在每个请求中重新生成会话标识预防了几种攻击的途径,但是不是每个请求需要这么做。因此,应用程序通常设法动态的确定在有较大风险的情况下,重新生成会话标识符。当站点的访问者权限上升时(比如,访问者在编辑你的个人信息前,要重新验证用户)或者敏感的会话参数发生改变时,可以考虑使用regenerateId()创建新的会话标识符。如果你调用了rememberMe()之后,就不需要调用regeneraterId(),因为前者已经调用了后者。如果用户成功登录了站点,调用rememberMe()方法来取代调用regenerateId()方法。

30.4.3.2. 会话劫持和会话固定

消除跨站脚本攻击(XSS)漏洞有利于防止会话劫持的发生。根据Secunia的统计,不管使用何种语言创建web应用程序,XSS问题经常发生。期望应用程序不存在跨站脚本攻击漏洞,还不如按照下面的最佳实践最小化损失,当攻击发生时。在跨站脚本攻击中,攻击者不需要直接访问受害者的网络。如果受害者已经存在一个会话 cookie,那么跨站脚本攻击的Javascript脚本会允许攻击者读取受害者的cookie并偷取会话。如果受害者还不存在会话cookie,利用跨站脚本攻击漏洞注入Javascript脚本,攻击者在受害者浏览器上创建一个已知会话标识符的cookie,然后在攻击者的系统中也创建同样的 cookie,这样就劫持受害者的会话。如果受害者访问了攻击者的站点,那么攻击者还能仿真受害者用户代理的一些其他特征。如果你的站点存在着XSS漏洞,攻击者就可能插入一段AJAX脚本,秘密的访问攻击者的站点,导致攻击者知道了受害者的浏览器特征,又知悉受害者站点的会话。然而,倘若站点开发者正确地设置了save_path选项,那么攻击者也不能任意地修改服务器端的PHP会话状态。

当第一次使用用户会话时,调用Zend_Session::regenerateId()不能防止会话固定攻击,除非你能辨别最初的会话是否是攻击者伪装成受害者。初听,这个跟前面所描述的是自相矛盾的,直到我们认为攻击者首先在你的站点上发起了一个真实的会话。如果会话第一次是被攻击者开启的,那么攻击者也就知道了初始化(regenerateId())后的结果(新的会话标识)。攻击者在XSS漏洞中使用这个新的会话标识,或者通过攻击者站点上的链接注入这个新的会话标识(只在use_only_cookies = off时有效)。

如果你能辨别使用相同会话标识符的受害者和攻击者,那么就可以直接处理会话劫持了。然而,这样的区分常常陷于可用性权衡的形式中,因为区别的方法常常是不严密的。举例来说,如果当前请求的IP与创建会话的请求的IP来自不同的国家,那么当前请求大概就是攻击者发起的。在以下的情形下,对于web应用程序就可能很难区别受害者和攻击者了:

  • - 攻击者首先在你的站点上发起一个会话,以获得一个合法的会话标识符

  • - 攻击者利用你的站点上的XSS漏洞,在受害者的浏览器上创建具有相同标识符且有效的会话cookie(也就是会话固定)

  • - 受害者和攻击者来自同一个上网代理(比如他们都处于一个大公司的同一个防火墙后面,像AOL)

下面的代码使得攻击者很难获得受害者当前的会话标识符,除非攻击者已经完成上面的第一二两步。

例 30.18. 匿名会话和会话固定

<?php
require_once 'Zend/Session.php';
$defaultNamespace = new Zend_Session_Namespace();
 
if (!isset($defaultNamespace->initialized))
{ 
    Zend_Session::regenerateId(); 
    $defaultNamespace->initialized = true;
} 
?>

30.4.4. rememberMe(integer $seconds)

通常,当用户代理结束时,会话也就结束了,比如当用户关闭了浏览器窗口。然而,当用户登录你的站点之后,他们会希望他们会话能保持24个小时,甚至更久。论坛软件通常给出一个选项,让用户选择他们的会话将持续多长时间。使用Zend_Session::rememberMe()向用户代理发送最新的带有生命期的会话cookie,默认的生命期为2个星期,除非你使用了Zend_Session::setOptions()方法修改了生命期值。当用户身份成功鉴定后或登录后使用该函数,有利于阻碍会话劫持、固定。

30.4.5. forgetMe()

此函数的作用与rememberMe()刚好相反,当用户代理终止会话时(例如用户关闭浏览器窗口),终止会话cookie的生命期。

30.4.6. sessionExists()

这个方法用来确定当前用户请求是否已经存在会话。这个方法可在会话开启之前使用,且这个方法独立于与Zend_Session和Zend_Session_Namespace的其他方法。

30.4.7. destroy(bool $remove_cookie = true, bool $readonly = true)

Zend_Session::destroy(),删除当前会话的所有数据。然而,PHP中的变量还未知情,所以你的会话命名空间(Zend_Session的实例)还是可读的。为了完成“登出”动作,设置可选的参数$remove_cookie为true(默认为true)来删除用户代理端的会话cookie。设置可选的参数$readonly为true,使Zend_Session实例和Zend_Session_Core中的方法失去写会话数据的能力。

[注意] Throws

默认的,$readonly是被激活的,之后写会话数据的动作,将会抛出一个异常。

30.4.8. stop()

这个方法只是更改了Zend_Session中的一个标志位,以阻止之后向会话数据存储器(也就是$_SESSION)中写数据。我们特别希望您能反馈关于这个特性的看法。当程序的执行转移到视图相关的代码上时,以免滥用,临时关闭Zend_Session_Namespace实例和Zend_Session中的方法向会话数据存储器写数据的能力,试图通过这些实例或方法向会话数据存储器写数据的动作,都将会抛出一个异常。

30.4.9. writeClose($readonly = true)

关闭会话,把$_SESSION数组中的数据写到后台的存储器中(文件、数据库),完成内部数据的转换。可选参数$readonly可移除写的能力(也就是,之后Zend_Session_Namespace或Zend_Session试图向会话数据存储器写数据,都会抛出一个异常)。

[注意] Throws

默认的,$readonly是被激活的,之后向会话数据存储器写数据的动作讲抛出异常。然而,一些遗留的应用程序期望$_SESSION在会话通过session_write_close()关闭后仍然可以写。虽然不是最佳的实践,但$readonly选项对有需要的人还是有用的。

30.4.10. expireSessionCookie()

该方法向客户端发送一个过期的会话cookie,以引起客户端删除会话cookie。通常这个技术被用来执行客户端登出请求。

30.4.11. setSaveHandler(Zend_Session_SaveHandler_Interface $interface)

对于大多数开发者来说默认的save handler已经足够了。这个方法只是以面向对象的方式包装了一下session_set_save_handler()函数。

30.4.12. namespaceIsset($namespace, $name = null)

这个方法用来检查某会话命名空间是否存在,或者某会话命名空间下的某个索引是否存在。

[注意] Throws

如果Zend_Session没有被标记为可读(比如在Zend_Session开启之前),将会抛出一个异常。

30.4.13. namespaceUnset($namespace, $name = null)

使用namespaceUnset($namespace)注销某个命名空间及其内容,而不用为某个命名空间创建Zend_Session实例,然后迭代它删除每个条目。如果被注销的变量为数组,且该数组包含了其他对象,而这些对象又被其他变量引用,这些对象仍然是可访问的。不要期望namespaceUnset方法会“深”注销/删除命名空间下条目的内容。更详细的解释,请参考PHP手册中的References Explained

[注意] Throws

如果命名空间不可读(比如执行了destroy()之后),将会抛出一个异常。

30.4.14. namespaceGet($namespace, $name = null)

这个方法返回$namespace命名空间的内容数组$name。如果你有合理的理由认为该方法是公有的,请反馈到我们的邮件列表:fw-auth@lists.zend.com。实际上,所有参与相关话题讨论的,我们都是欢迎的。

[注意] Throws

如果Zend_Session没有被标记为可读(比如在Zend_Session开启之前),将会抛出一个异常。

30.4.15. getIterator()

使用getIterator()方法,可获得一个可迭代的包含所有命名空间名字的数组。

例 30.19. 注销所有的命名空间

<?php
foreach(Zend_Session::getIterator() as $space) {
    try {
        $core->namespaceUnset($space);
    } catch (Zend_Session_Exception $e) {
        return; // possible if Zend_Session::stop() has been executed
    }
}

?>
[注意] Throws

如果Zend_Session_Core没有被标记为可读(比如在Zend_Session_Core开启之前),将会抛出一个异常。