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

Mar*_*dez 12 php session

介绍

从PHP 5.5.2开始,有一个运行时配置选项(session.use_strict_mode),用于防止恶意客户端进行会话固定.启用此选项并使用本机会话处理程序(文件)时,PHP将不接受先前在会话存储区域中不存在的任何传入会话ID,如下所示:

$ 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
Run Code Online (Sandbox Code Playgroud)

(session.use_strict_mode如果已禁用,则响应将包含Set-Cookie标头,并且sess_madeupkey会在sessions目录中创建文件)

问题

我正在实现自定义会话处理程序,我非常希望它遵守严格模式,但是界面使其变得困难.

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

总而言之,为了匹配本机行为,我需要回答的问题是:

  1. 从上下文来看read($session_id),如何区分新建的会话ID或来自HTTP请求的会话ID?

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

Nar*_*arf 5

看到有一个已经被接受的答案,我将其作为尚未提及的替代方法提供。


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

不幸的是,这在PHP 5上不起作用,因为PHP 5中的用户空间处理程序必须自己实现use_strict_mode=1功能。
这里一条捷径,但让我先回答你的直接提问...

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

乍一看,这似乎确实有帮助,但是您在这里遇到的问题是,这read()根本没有用。主要是由于以下两个原因:

  • 此时,会话已被初始化。您要拒绝不存在的会话ID,不进行初始化然后丢弃它们。
  • 读取不存在的会话ID的空数据和/或仅返回新创建的ID的空数据之间没有区别。因此,即使您知道您正在处理一个不存在的会话ID的呼叫,也无济于事。

您可以session_regenerate_id()从内部调用read(),但是可能会有意想不到的副作用,或者,如果您确实希望这些副作用,则可能会使您的逻辑变得非常复杂。
例如,基于文件的存储将围绕文件描述符构建,并且应该从inside read(),但随后session_regenerate_id()将直接调用write(),此时您将没有要写入的(正确)文件描述符。

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

在最长的时间内,我讨厌用户空间处理程序无法发出错误状态信号,直到我发现您可以做到。
事实证明,它实际上是设计用来处理布尔truefalse如成功,失败。只是PHP如何处理此问题而存在一个非常细微的错误...

在内部,PHP使用的值0,并-1分别标记成功和失败,但处理转换为逻辑truefalse为用户空间是错误的,实际上暴露此内部行为,并留下它无证。
此问题已在PHP 7中修复,但仍保留给PHP 5,因为该错误非常非常老,修复后会导致巨大的BC中断。此PHP RFC中的更多信息提出了针对PHP 7的修复程序。

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


现在我提到的快捷方式...

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

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

您会注意到,我把它做成了一个抽象类-这只是因为我太懒了,无法在此处编写整个处理程序,除非您确实实现了这些SessionHandlerInterface方法,否则PHP会忽略您的处理程序-只是在SessionHandler不覆盖任何方法的情况下进行扩展即可与根本不使用自定义处理程序的方式相同(将执行构造函数代码,但默认的PHP实现将保留严格的模式逻辑)。

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


Mar*_*dez 4

更新(2017-03-19)

我最初的实现委托session_regenerate_id()生成新的会话 ID 并在适当的时候设置 cookie 标头。从 PHP 7.1.2 开始,无法再从会话处理程序[1]内部调用此方法。Decent Dabbler 还报告说这种方法在 PHP 5.5.9 中不起作用[2]

该方法的以下变体read()避免了这个陷阱,但有些混乱,因为它必须自行设置 cookie 标头。

/**
 * {@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) ?: '';
}
Run Code Online (Sandbox Code Playgroud)

FWIW 已知原始实现可以在 PHP 7.0.x 下工作

原答案

结合从 Dave 的答案中获得的见解(即扩展类\SessionHandler而不是实现来\SessionHandlerInterface窥视create_sid并解决第一个障碍)和Rasmus Schultz 对会话生命周期的精细实地研究,我想出了一个非常令人满意的解决方案:它不不会因为 SID 生成而妨碍自身,也不会手动设置任何 cookie,也不会在客户端代码链上浪费时间。为了清楚起见,仅显示相关方法:

<?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
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

以及测试脚本:

<?php

session_set_save_handler(new MySessionHandler(), true);

session_start();

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

echo $_SESSION['visits'];
Run Code Online (Sandbox Code Playgroud)