php - pasar - Garantizar el modo estricto de sesión en la implementación personalizada de SessionHandlerInterface
php session put (4)
Introducción
Desde PHP 5.5.2 hay una opción de configuración en tiempo de ejecución ( session.use_strict_mode ) que está destinada a evitar la fijación de sesiones por parte de clientes malintencionados. Cuando esta opción está habilitada y se utiliza el controlador de sesión nativo (archivos), PHP no aceptará ninguna ID de sesión entrante que no existiera previamente en el área de almacenamiento de la sesión, como se muestra a continuación:
$ 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
(con session.use_strict_mode
deshabilitado, la respuesta no hubiera incluido un encabezado Set-Cookie
y se habría creado un archivo sess_madeupkey
en el directorio de sesiones)
Problema
Estoy en el proceso de implementar un controlador de sesión personalizado y me gustaría que se adhiera al modo estricto, sin embargo, la interfaz lo dificulta.
Cuando se llama a session_start()
, se MyHandler::read($session_id)
en la línea, pero $session_id
puede ser el valor obtenido de la cookie de sesión o una nueva ID de sesión. El manejador necesita saber la diferencia, porque en el primer caso se debe generar un error si no se puede encontrar el ID de la sesión. Además, de acuerdo con la read($session_id)
especificada read($session_id)
debe devolver el contenido de la sesión o una cadena vacía (para sesiones nuevas), pero parece que no hay forma de generar un error en la cadena.
Para resumir, las preguntas que necesito responder para que coincida con el comportamiento nativo son:
Desde el contexto de
read($session_id)
, ¿cómo puedo diferenciar entre una ID de sesión recién acuñada o una ID de sesión que proviene de la solicitud HTTP?Dado un ID de sesión que proviene de la solicitud HTTP y suponiendo que no se encontró en el área de almacenamiento, ¿cómo señalaría un error al motor PHP para que vuelva a llamar a
read($session_id)
con una nueva ID de sesión?
Supongo que podría, como enfoque más simple, ampliar un poco la implementación de ejemplo de esta manera:
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;
}
Dentro del método de write
puede generar una nueva ID de sesión y forzarla de vuelta al cliente.
No es lo más limpio, de verdad. Esto se refiere a la configuración del controlador de guardar la sesión, por lo que "dominio del usuario" solo debe proporcionar la implementación del almacenamiento, O la interfaz del administrador debe definir un método de validación para que se llame automáticamente, probablemente antes de su read
. De todos modos esto ha sido discutido aquí .
No lo he probado, así que puede funcionar o no.
La clase SessionHandler
predeterminada se puede extender. Esta clase contiene un método extra relevante que la interfaz no tiene, es decir, create_sid()
. Esto se llama cuando PHP genera una nueva ID de sesión. Por lo tanto, debería ser posible usar esto para distinguir entre una nueva sesión y un ataque; algo como:
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
}
Este enfoque puede necesitar relleno con otra bandera o dos para manejar la regeneración de ID de sesión legítima, si alguna vez hace esto.
Con respecto al problema de manejar el error correctamente, experimentaría lanzando una excepción. Si esto no produce nada útil, lo haría en el nivel de la aplicación, devolviendo un valor fijo para los datos de la sesión en sí mismo que podría verificar y luego manejarlo en la aplicación generando una nueva ID o destruyendo la sesión y presentando un error al usuario.
Actualización (2017-03-19)
Mi implementación original se delegó en session_regenerate_id()
para generar nuevas ID de sesión y configurar el encabezado de la cookie cuando corresponda. A partir de PHP 7.1.2 este método ya no se puede invocar desde el controlador de sesión [1] . Decente Dabbler también informó que este enfoque no funcionará en PHP 5.5.9 [2] .
La siguiente variación del método de read()
evita este inconveniente, pero es un poco más complicado, ya que tiene que establecer el propio encabezado de la 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 se sabe que la implementación original funciona con PHP 7.0.x
Respuesta original
Combinando la información obtenida de la respuesta de Dave (es decir, ampliando la clase /SessionHandler
lugar de implementar /SessionHandlerInterface
para create_sid
en create_sid
y resolver el primer obstáculo) y este fino estudio de campo del ciclo de vida de la sesión de Rasmus Schultz, se me ocurrió una solución bastante satisfactoria : no se obstruye con la generación de SID, ni establece ninguna cookie manualmente, ni inicia el ciclo de la cadena al código del cliente. Solo los métodos relevantes se muestran para mayor claridad:
<?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
}
}
Nota: la clase ignora la opción session.use_strict_mode
pero siempre sigue el comportamiento estricto (esto es realmente lo que quiero). Estos son los resultados de las pruebas en mi implementación más completa:
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
Y el script de prueba:
<?php
session_set_save_handler(new MySessionHandler(), true);
session_start();
if (!isset($_SESSION[''visits''])) {
$_SESSION[''visits''] = 1;
} else {
$_SESSION[''visits'']++;
}
echo $_SESSION[''visits''];
Al ver que hay una respuesta ya aceptada, estoy proporcionando esto como una alternativa aún no mencionada.
A partir de PHP 7, si su controlador de sesión implementa un método validateId()
, PHP lo usará para determinar si se debe generar o no una nueva ID.
Desafortunadamente, eso no funciona en PHP 5 donde los manejadores de use_strict_mode=1
tienen que implementar la funcionalidad use_strict_mode=1
por sí mismos.
Hay un atajo, pero déjame responder tus preguntas directas primero ...
Desde el contexto de
read($session_id)
, ¿cómo puedo diferenciar entre una ID de sesión recién acuñada o una ID de sesión que proviene de la solicitud HTTP?
A primera vista, parece que esto ayudaría, pero el problema que tendrá aquí es que read()
no es útil en absoluto para esto. Principalmente por las siguientes dos razones:
- En este punto, la sesión ya se está inicializando. Desea rechazar identificadores de sesión inexistentes, no inicializarlos y luego descartarlos.
- No hay diferencia entre leer datos vacíos para una ID de sesión inexistente y / o simplemente devolver datos vacíos para una ID recién creada. Por lo tanto, incluso si sabe que está procesando una llamada para una identificación de sesión inexistente, eso no le ayuda mucho.
Podrías llamar a session_regenerate_id()
desde adentro de read()
, pero eso puede tener efectos secundarios inesperados, o complicar en gran medida tu lógica si esperas esos efectos secundarios ...
Por ejemplo, el almacenamiento basado en archivos se generaría alrededor de los descriptores de archivos y estos deberían estar abiertos desde adentro read()
, pero luego session_regenerate_id()
llamará directamente a write()
y no tendrá el descriptor de archivo (correcto) para escribir en en ese punto.
Dado un ID de sesión que proviene de la solicitud HTTP y suponiendo que no se encontró en el área de almacenamiento, ¿cómo señalaría un error al motor PHP para que vuelva a llamar a
read($session_id)
con una nueva ID de sesión?
Durante mucho tiempo, odiaba que los manejadores de espacio de usuario no pudieran indicar las condiciones de error, hasta que descubrí que podías hacerlo.
Como resultado, de hecho fue diseñado para manejar boolean true
, false
como success, failure. Es solo que había un error muy sutil en cómo manejó esto PHP ...
Internamente, PHP usa los valores 0
y -1
para marcar el éxito y el fracaso, respectivamente, pero la lógica que manejó la conversión a true
, false
para el espacio de usuario fue errónea y de hecho expuso este comportamiento interno y lo dejó sin documentar.
Esto se arregló en PHP 7, pero se dejó como está para PHP 5 ya que el error es muy, muy antiguo y resultaría en grandes rupturas BC cuando se solucionó. Más información en este PHP RFC que propuso la solución para PHP 7.
Entonces, para PHP 5, de hecho puedes devolver int(-1)
desde los métodos del manejador de sesión para señalar un error, pero eso no es realmente útil para la aplicación de "modo estricto", ya que resulta en un comportamiento completamente diferente: emite un E_WARNING
y detiene la inicialización de la sesión.
Ahora para ese atajo que mencioné ...
Esto no es del todo obvio, y de hecho es muy extraño, pero ext / session no solo lee las cookies y las maneja por sí mismo: en realidad usa el $_COOKIE
_COOKIE, y eso significa que puede manipular $_COOKIE
para alterar la sesión comportamiento del manejador!
Entonces, aquí hay una solución que incluso es compatible con 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);
}
}
Notarás que hice una clase abstracta, eso es solo porque soy demasiado perezoso para escribir todo el manejador aquí y, a menos que realmente implementes los métodos SessionHandlerInterface
, PHP ignorará tu manejador, simplemente extender SessionHandler
sin anular ningún método es manejado de la misma manera que no se usa un controlador personalizado en absoluto (se ejecutará el código del constructor, pero la lógica del modo estricto seguirá siendo la implementación predeterminada de PHP).
TL; DR: compruebe si tiene datos asociados con $_COOKIE[ini_get(''session.name'')]
antes de llamar a session_start()
, y desarme la cookie si no lo hace - esto le dice a PHP que se comporte como si hubiera recibido no hay ninguna cookie de sesión, lo que desencadena una nueva generación de ID de sesión. :)