首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >在自定义SessionHandlerInterface实现中确保会话严格模式

在自定义SessionHandlerInterface实现中确保会话严格模式
EN

Stack Overflow用户
提问于 2016-12-31 20:05:41
回答 4查看 1.9K关注 0票数 12

简介

由于PHP5.5.2有一个运行时配置选项(模式),用于防止恶意客户端进行会话固定。当启用此选项并使用本地会话处理程序 (文件)时,PHP将不会接受以前在会话存储区域中不存在的任何传入会话ID,如下所示:

代码语言:javascript
运行
复制
$ curl -I -H "Cookie:PHPSESSID=madeupkey;" localhost
HTTP/1.1 200 OK
Cache-Control: no-store, no-cache, must-revalidate
Connection: close
Content-type: text/html; charset=UTF-8
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Host: localhost
Pragma: no-cache
Set-Cookie: PHPSESSID=4v3lkha0emji0kk6lgl1lefsi1; path=/  <--- looky

(在禁用session.use_strict_mode之后,响应将使而不是包含一个Set-Cookie头,并在会话目录中创建一个sess_madeupkey文件)

问题

我正处于实现自定义会话处理程序的过程中,我非常希望它能够坚持严格的模式,但是接口会使它变得很困难。

当调用session_start()时,MyHandler::read($session_id)会一直被调用,但是$session_id可以是--或者是从会话cookie 获取的值,或者是--一个新的会话ID。处理程序需要知道区别,因为在前一种情况下,如果找不到会话ID,就必须引发错误。此外,根据规范,read($session_id)必须返回会话内容或空字符串(对于新会话),但是似乎没有办法在链上引发错误。

总之,我需要回答的问题是:

  1. read($session_id)的上下文中,如何区分来自HTTP请求的新创建的会话ID和会话ID之间的区别?
  2. 给定来自HTTP请求的会话ID,并假设在存储区域中找不到它,我如何向PHP发出错误信号,以便它使用新的会话ID再次调用read($session_id)
EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2017-01-09 22:32:14

更新(2017-03-19)

我最初的实现是在session_regenerate_id()上委派的,用于生成新的会话ID,并在适当时设置cookie头。从PHP7.1.2开始,不能再从会话处理程序[1]中调用此方法。体面的达布勒还报告说,这种方法在PHP5.5.9[2]中行不通。

read()方法的以下变体避免了这一缺陷,但有些混乱,因为它必须设置cookie标头本身。

代码语言:javascript
运行
复制
/**
 * {@inheritdoc}
 */
public function open($save_path, $name)
{
    // $name is the desired name for the session cookie, as specified
    // in the php.ini file. Default value is 'PHPSESSID'.
    // (calling session_regenerate_id() used to take care of this)
    $this->cookieName = $name;

    // the handling of $save_path is implementation-dependent
}

/**
 * {@inheritdoc}
 */
public function read($session_id)
{
    if ($this->mustRegenerate($session_id)) {
        // Manually set a new ID for the current session
        session_id($session_id = $this->create_sid());

        // Manually set the 'Cookie: PHPSESSID=xxxxx;' header
        setcookie($this->cookieName, $session_id);
    }

    return $this->getSessionData($session_id) ?: '';
}

FWIW已知最初的实现在PHP7.0.x下工作

原始答案

结合从Dave的答案中获得的洞察力(即扩展\SessionHandler类而不是实现\SessionHandlerInterface以窥探create_sid并解决第一个障碍)和这个Rasmus Schultz会话生命周期的精细现场研究,我想出了一个非常令人满意的解决方案:它不会将自己封装在SID生成中,也不会手动设置任何cookie,也不会将桶向上插入客户端代码。为了清楚起见,只显示了相关的方法:

代码语言:javascript
运行
复制
<?php

class MySessionHandler extends \SessionHandler
{
    /**
     * A collection of every SID generated by the PHP internals
     * during the current thread of execution.
     *
     * @var string[]
     */
    private $new_sessions;

    public function __construct()
    {
        $this->new_sessions = [];
    }

    /**
     * {@inheritdoc}
     */
    public function create_sid()
    {
        $id = parent::create_sid();

        // Delegates SID creation to the default
        // implementation but keeps track of new ones
        $this->new_sessions[] = $id;

        return $id;
    }

    /**
     * {@inheritdoc}
     */
    public function read($session_id)
    {
        // If the request had the session cookie set and the store doesn't have a reference
        // to this ID then the session might have expired or it might be a malicious request.
        // In either case a new ID must be generated:
        if ($this->cameFromRequest($session_id) && null === $this->getSessionData($session_id)) {
            // Regenerating the ID will call destroy(), close(), open(), create_sid() and read() in this order.
            // It will also signal the PHP internals to include the 'Set-Cookie' with the new ID in the response.
            session_regenerate_id(true);

            // Overwrite old ID with the one just created and proceed as usual
            $session_id = session_id();
        }

        return $this->getSessionData($session_id) ?: '';
    }

    /**
     * @param string $session_id
     *
     * @return bool Whether $session_id came from the HTTP request or was generated by the PHP internals
     */
    private function cameFromRequest($session_id)
    {
        // If the request had the session cookie set $session_id won't be in the $new_sessions array
        return !in_array($session_id, $this->new_sessions);
    }

    /**
     * @param string $session_id
     *
     * @return string|null The serialized session data, or null if not found
     */
    private function getSessionData($session_id)
    {
        // implementation-dependent
    }
}

注意:类忽略session.use_strict_mode选项,但始终遵循严格的行为(这实际上是我想要的)。以下是我所看到的更完整的实现中的测试结果:

代码语言:javascript
运行
复制
marcel@werkbox:~$ curl -i -H "Cookie:PHPSESSID=madeupkey" localhost/tests/visit-counter.php
HTTP/1.1 200 OK
Server: nginx/1.11.6
Date: Mon, 09 Jan 2017 21:53:05 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Set-Cookie: PHPSESSID=c34ovajv5fpjkmnvr7q5cl9ik5; path=/      <--- Success!
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache

1

marcel@werkbox:~$ curl -i -H "Cookie:PHPSESSID=c34ovajv5fpjkmnvr7q5cl9ik5" localhost/tests/visit-counter.php
HTTP/1.1 200 OK
Server: nginx/1.11.6
Date: Mon, 09 Jan 2017 21:53:14 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache

2

以及测试脚本:

代码语言:javascript
运行
复制
<?php

session_set_save_handler(new MySessionHandler(), true);

session_start();

if (!isset($_SESSION['visits'])) {
    $_SESSION['visits'] = 1;
} else {
    $_SESSION['visits']++;
}

echo $_SESSION['visits'];
票数 4
EN

Stack Overflow用户

发布于 2017-03-20 10:40:49

鉴于有一个已经被接受的答案,我提供了一个尚未提及的替代方案。

从PHP 7开始,如果会话处理程序实现了一个validateId()方法,PHP将使用它来确定是否应该生成新的ID。

不幸的是,这并不适用于PHP 5,因为在PHP 5中,用户空间处理程序必须自己实现use_strict_mode=1功能。

有条捷径,但让我先回答你的直接问题.

read($session_id)的上下文中,如何区分来自HTTP请求的新创建的会话ID和会话ID之间的区别?

乍一看,这似乎会有所帮助,但这里的问题是,read()对此毫无用处。主要有以下两个原因:

  • 此时,会话已被初始化。您希望拒绝不存在的会话ID,而不是初始化它们,然后丢弃它们.
  • 读取不存在的会话ID的空数据和/或只返回新创建的ID的空数据没有区别,因此,即使您知道正在处理对不存在的会话ID的调用,这对您也没有多大帮助。

您可以从session_regenerate_id()内部调用read(),但这可能会产生意外的副作用,或者如果您预期到这些副作用,则会使您的逻辑变得非常复杂.

例如,基于文件的存储将围绕文件描述符构建,这些描述符应该在read()内部打开,但是session_regenerate_id()将直接调用write(),此时您将没有(正确的)文件描述符可写入。

给定来自HTTP请求的会话ID,并假设在存储区域中找不到它,我如何向PHP发出错误信号,以便它使用新的会话ID再次调用read($session_id)

有很长一段时间,我讨厌用户空间处理程序不能发出错误条件的信号,直到我发现您可以这样做。

事实证明,它实际上是设计用来将布尔型truefalse作为成功或失败处理的。只是PHP处理这个问题的方式有一个非常微妙的错误.

在内部,PHP分别使用0-1值来标识成功和失败,但是处理转换为truefalse用于用户空间的逻辑是错误的,实际上暴露了这个内部行为,并且没有文档化。

这在PHP 7中是固定的,但与PHP 5一样,因为这个bug非常非常老,修复后会导致大量BC中断。在这个PHP RFC中有更多的信息,其中提出了针对PHP7的修复。

因此,对于PHP 5,您实际上可以从内部会话处理程序方法中返回int(-1)来表示错误,但这对于“严格模式”的强制执行并不是很有用,因为它会导致完全不同的行为--它会发出E_WARNING并停止会话初始化。

我提到的那条捷径..。

这一点一点也不明显,实际上也很奇怪,但是ext/session并不只是读取cookie并单独处理它们--它实际上使用了$_COOKIE超级全局,这意味着您可以操纵$_COOKIE来改变会话处理程序的行为!

因此,这里有一个甚至与PHP 7向前兼容的解决方案:

代码语言:javascript
运行
复制
abstract class StrictSessionHandler
{
    private $savePath;
    private $cookieName;

    public function __construct()
    {
        $this->savePath = rtrim(ini_get('session.save_path'), '\\/').DIRECTORY_SEPARATOR;

        // Same thing that gets passed to open(), it's actually the cookie name
        $this->cookieName = ini_get('session.name');

        if (PHP_VERSION_ID < 70000 && isset($_COOKIE[$this->cookieName]) && ! $this->validateId($_COOKIE[$this->cookieName])) {
            unset($_COOKIE[$this->cookieName]);
        }
    }

    public function validateId($sessionId)
    {
        return is_file($this->savePath.'sess_'.$sessionId);
    }
}

您将注意到,我使它成为一个抽象类--这只是因为我懒得在这里编写整个处理程序,除非您确实实现了SessionHandlerInterface方法,否则PHP将忽略您的处理程序--只需简单地扩展SessionHandler而不覆盖任何方法,就像根本不使用自定义处理程序一样(将执行构造函数代码,但严格的模式逻辑将保留在默认的PHP实现中)。

TL;DR:在调用$_COOKIE[ini_get('session.name')]之前检查是否有与session_start()相关的数据,如果没有,则取消cookie --这告诉PHP要表现得好像根本没有接收到会话cookie,从而触发新的会话ID生成。:)

票数 5
EN

Stack Overflow用户

发布于 2017-01-09 15:23:52

我还没有测试过这个,所以它可能有用,也可能不起作用。

可以扩展默认的SessionHandler类。该类包含接口没有的相关额外方法,即create_sid()。这在PHP生成新会话ID时调用。因此,应该可以使用它来区分新会话和攻击;如下所示:

代码语言:javascript
运行
复制
class MySessionHandler extends \SessionHandler
{
    private $isNewSession = false;

    public function create_sid()
    {
        $this->isNewSession = true;

        return parent::create_sid();
    }

    public function read($id)
    {
        if ($this->dataStore->haveExistingSession($id)) {
            return $this->getSessionData($id);
        }

        if ($this->isNewSession) {
            $this->dataStore->createNewSession($id);
        }

        return '';
    }

    // ...rest of implementation
}

如果您曾经这样做过,这种方法可能需要使用另一个或两个标记来处理合法的会话ID重新生成。

关于优雅地处理错误的问题,我会尝试抛出一个异常。如果这不能产生任何有用的结果,我将在应用程序级别上这样做,方法是为会话数据本身返回一个可以检查的固定值,然后由生成新ID破坏会议在应用程序中处理它,并向用户提供一个错误。

票数 3
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/41411367

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档