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

时间:2016-12-31 20:05:42

标签: php session

简介

自PHP 5.5.2起,有一个运行时配置选项(session.use_strict_mode),用于防止恶意客户端进行会话固定。启用此选项并使用native session handler(文件)时,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

(禁用session.use_strict_mode后,响应将包含Set-Cookie标头,并且会在会话目录中创建sess_madeupkey文件)< / p>

问题

我正在实施custom session handler并且我非常希望遵守严格模式,但界面却很难。

调用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?

4 个答案:

答案 0 :(得分:4)

更新(2017-03-19)

我的原始实现委托session_regenerate_id()生成新会话ID并在适当时设置cookie标头。从PHP 7.1.2开始,无法再从会话处理程序[1]内部调用此方法。体面的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) ?: '';
}

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

原始答案

结合从Dave的答案中获得的洞察力(即扩展\SessionHandler类而不是实现\SessionHandlerInterface以窥视create_sid并解决第一道障碍)和此{ {3}}我提出了一个非常令人满意的解决方案:它不会使用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
    }
}

注意:该类忽略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

测试脚本:

<?php

session_set_save_handler(new MySessionHandler(), true);

session_start();

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

echo $_SESSION['visits'];

答案 1 :(得分:3)

我没有对此进行测试,因此可能会或可能不会。

可以扩展默认的SessionHandler类。该类包含接口不具有的相关额外方法,即create_sid()。当PHP生成新的会话ID时调用此方法。因此,应该可以使用它来区分新会话和攻击;类似的东西:

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重新生成。

关于优雅地处理错误的问题,我会尝试抛出异常。如果这不会产生任何有用的东西,我会在应用程序级别执行此操作,方法是返回您可以检查的会话数据本身的固定值,然后通过generating a new ID或{{在应用程序中处理它3}}并向用户显示错误。

答案 2 :(得分:3)

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

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

不幸的是,这在用户空间处理程序必须自己实现use_strict_mode=1功能的PHP 5上不起作用。
的快捷方式,但让我先回答你的直接问题......

  

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

乍一看,看起来确实会有所帮助,但是你在这里遇到的问题是read()根本没有用处。主要有以下两个原因:

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

您可以从session_regenerate_id()内部调用read(),但这可能会产生意想不到的副作用,或者如果您确实期望这些副作用会使您的逻辑大为复杂......
例如,基于文件的存储将围绕文件描述符构建,那些应该从read()内部打开,但是session_regenerate_id()将直接调用write()而你将没有(正确的) )此时写入的文件描述符。

  

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

最长的时间,我讨厌用户空间处理程序无法发出错误信号,直到我发现你可以这样做。
事实证明,事实上它设计为处理布尔truefalse成功,失败。这只是PHP处理这个问题的一个非常微妙的错误......

在内部,PHP使用值0-1分别标记成功和失败,但处理用户空间转换为truefalse的逻辑是错误的,实际上暴露了这种内部行为,并将其保留为无证件 这是在PHP 7中修复的,但保留为PHP 5,因为bug非常非常旧,并且在修复时会导致巨大的BC中断。 this 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);
    }
}

你会注意到我把它变成了一个抽象类 - 这只是因为我懒得在这里编写整个处理程序,除非你实际实现了SessionHandlerInterface方法,否则PHP会忽略你的处理程序 - 只是扩展SessionHandler而不覆盖任何方法的处理方式与完全不使用自定义处理程序相同(构造函数代码将被执行,但严格模式逻辑将保留在默认的PHP实现中)。

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

答案 3 :(得分:2)

我想你可以,作为最简单的方法,像这样扩展一点the sample implementation

private $validSessId = false;

public function read($id)
{
    if (file_exists("$this->savePath/sess_$id")) {
        $this->validSessId = true;
        return (string)@file_get_contents("$this->savePath/sess_$id");
    }
    else {    
        return '';
    }
}

public function write($id, $data)
{
    if (! $this->validSessId) {
        $id = $this->generateNewSessId();
        header("Set-Cookie:PHPSESSID=$id;");
    }

    return file_put_contents("$this->savePath/sess_$id", $data) === false ? false : true;
}

write方法中,您可以生成新的会话ID并将其强制回客户端。

真的,这不是最干净的事情。这涉及设置会话 save 处理程序,因此我们“userland”应该只提供存储实现,或者处理程序接口应该定义一个自动调用的验证方法,可能在read之前。无论如何,这已被讨论here