自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)
必须返回会话内容或空字符串(对于新会话),但似乎无法在链上引发错误。
总而言之,为了匹配本机行为,我需要回答的问题是:
在read($session_id)
的上下文中,如何区分新建的会话ID或来自HTTP请求的会话ID?
鉴于来自HTTP请求的会话ID并假设在存储区域中找不到它,我将如何向PHP引擎发出错误信号,以便它再次调用read($session_id)
一个新的会话ID?
答案 0 :(得分:4)
我的原始实现委托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()
根本没有用处。主要有以下两个原因:
您可以从session_regenerate_id()
内部调用read()
,但这可能会产生意想不到的副作用,或者如果您确实期望这些副作用会使您的逻辑大为复杂......
例如,基于文件的存储将围绕文件描述符构建,那些应该从read()
内部打开,但是session_regenerate_id()
将直接调用write()
而你将没有(正确的) )此时写入的文件描述符。
给定来自HTTP请求的会话ID并假设在存储区域中找不到它,我将如何向PHP引擎发出错误信号,以便它可以使用新会话再次调用
read($session_id)
ID?
最长的时间,我讨厌用户空间处理程序无法发出错误信号,直到我发现你可以这样做。
事实证明,事实上它设计为处理布尔true
,false
成功,失败。这只是PHP处理这个问题的一个非常微妙的错误......
在内部,PHP使用值0
和-1
分别标记成功和失败,但处理用户空间转换为true
,false
的逻辑是错误的,实际上暴露了这种内部行为,并将其保留为无证件
这是在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。