简介
由于PHP5.5.2有一个运行时配置选项(模式),用于防止恶意客户端进行会话固定。当启用此选项并使用本地会话处理程序 (文件)时,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
文件)
问题
我正处于实现自定义会话处理程序的过程中,我非常希望它能够坚持严格的模式,但是接口会使它变得很困难。
当调用session_start()
时,MyHandler::read($session_id)
会一直被调用,但是$session_id
可以是--或者是从会话cookie 获取的值,或者是--一个新的会话ID。处理程序需要知道区别,因为在前一种情况下,如果找不到会话ID,就必须引发错误。此外,根据规范,read($session_id)
必须返回会话内容或空字符串(对于新会话),但是似乎没有办法在链上引发错误。
总之,我需要回答的问题是:
read($session_id)
的上下文中,如何区分来自HTTP请求的新创建的会话ID和会话ID之间的区别?read($session_id)
?发布于 2017-01-09 14:32:14
更新(2017-03-19)
我最初的实现是在session_regenerate_id()
上委派的,用于生成新的会话ID,并在适当时设置cookie头。从PHP7.1.2开始,不能再从会话处理程序[1]中调用此方法。体面的达布勒还报告说,这种方法在PHP5.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已知最初的实现在PHP7.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
}
}
注意:类忽略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'];
发布于 2017-03-20 02:40:49
鉴于有一个已经被接受的答案,我提供了一个尚未提及的替代方案。
从PHP 7开始,如果会话处理程序实现了一个validateId()
方法,PHP将使用它来确定是否应该生成新的ID。
不幸的是,这并不适用于PHP 5,因为在PHP 5中,用户空间处理程序必须自己实现use_strict_mode=1
功能。
有条捷径,但让我先回答你的直接问题.
从
read($session_id)
的上下文中,如何区分来自HTTP请求的新创建的会话ID和会话ID之间的区别?
乍一看,这似乎会有所帮助,但这里的问题是,read()
对此毫无用处。主要有以下两个原因:
您可以从session_regenerate_id()
内部调用read()
,但这可能会产生意外的副作用,或者如果您预期到这些副作用,则会使您的逻辑变得非常复杂.
例如,基于文件的存储将围绕文件描述符构建,这些描述符应该在read()
内部打开,但是session_regenerate_id()
将直接调用write()
,此时您将没有(正确的)文件描述符可写入。
给定来自HTTP请求的会话ID,并假设在存储区域中找不到它,我如何向PHP发出错误信号,以便它使用新的会话ID再次调用
read($session_id)
?
有很长一段时间,我讨厌用户空间处理程序不能发出错误条件的信号,直到我发现您可以这样做。
事实证明,它实际上是设计用来将布尔型true
、false
作为成功或失败处理的。只是PHP处理这个问题的方式有一个非常微妙的错误.
在内部,PHP分别使用0
和-1
值来标识成功和失败,但是处理转换为true
、false
用于用户空间的逻辑是错误的,实际上暴露了这个内部行为,并且没有文档化。
这在PHP 7中是固定的,但与PHP 5一样,因为这个bug非常非常老,修复后会导致大量BC中断。在这个PHP RFC中有更多的信息,其中提出了针对PHP7的修复。
因此,对于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()
相关的数据,如果没有,则取消cookie --这告诉PHP要表现得好像根本没有接收到会话cookie,从而触发新的会话ID生成。:)
发布于 2017-01-09 07:23:52
我还没有测试过这个,所以它可能有用,也可能不起作用。
可以扩展默认的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重新生成。
关于优雅地处理错误的问题,我会尝试抛出一个异常。如果这不能产生任何有用的结果,我将在应用程序级别上这样做,方法是为会话数据本身返回一个可以检查的固定值,然后由生成新ID或破坏会议在应用程序中处理它,并向用户提供一个错误。
https://stackoverflow.com/questions/41411367
复制