Skip to content

Commit

Permalink
recreate session when not random enough
Browse files Browse the repository at this point in the history
  • Loading branch information
juliangut committed Sep 16, 2016
1 parent 6c157b2 commit d949bef
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 20 deletions.
16 changes: 11 additions & 5 deletions README.md
Expand Up @@ -18,18 +18,24 @@ Generates a 80 character long session_id using `random_bytes`, a truly cryptogra

#### Important considerations

Be aware that this middleware prevents `session_start` and PHP session mechanisms from automatically send any kind of header to the client (including session cookie and caching). Middleware appends a `Set-Cookie` header to the response object instead.
Be aware that this middleware needs some session `ini` settings to be set to specific values:

`session.use_trans_sid` to `false`
`session.use_cookies` to `true`
`session.use_only_cookies` to `true`
`session.use_strict_mode` to `false`
`session.cache_limiter` to '' (empty string)

This values will prevent session headers to be automatically sent to user. **It's the developer's responsibility to include corresponding cache headers in response object**, which should be the case in the first place instead of relying on PHP environment settings.

> You can use [juliangut/cacheware](https://github.com/juliangut/cacheware) which will automatically set the corrent session ini settings and add the corresponding cache headers to response object.
By using `session_regenerate_id()` during execution cryptographically secure session ID will be replaced by default PHP `session.hash_function` generated ID (not really secure). To prevent this from happening use `\Jgut\Middleware\Session` helper method `regenerateSessionId()` instead:

```php
\Jgut\Middleware\Session::regenerateSessionId();
```

Value of `session.cache_limiter` and `session.cache_expire` get discarded so no cache headers are sent. **It's the developer's responsibility to include corresponding cache headers in response object**, which should be the case in the first place instead of relying on PHP environment settings.

> You can use [juliangut/cacheware](https://github.com/juliangut/cacheware) which will automatically add the corresponding cache headers to response object.
## Installation

### Composer
Expand Down
30 changes: 27 additions & 3 deletions src/SessionWare.php
Expand Up @@ -31,6 +31,8 @@ class SessionWare implements EmitterAwareInterface

const SESSION_TIMEOUT_KEY_DEFAULT = '__SESSIONWARE_TIMEOUT_TIMESTAMP__';

const SESSION_ID_LENGTH = 80;

/**
* @var array
*/
Expand Down Expand Up @@ -138,6 +140,10 @@ protected function startSession(ServerRequestInterface $request)
$this->manageSessionTimeout();

$this->populateSession($this->initialSessionParams);

if (strlen(session_id()) !== static::SESSION_ID_LENGTH) {
$this->recreateSession();
}
}

/**
Expand Down Expand Up @@ -335,14 +341,32 @@ protected function manageSessionTimeout()

session_start();

$this->sessionId = session_id();

$this->emit(Event::named('post.session_timeout'), session_id());
}

$_SESSION[$this->sessionTimeoutKey] = time() + $this->sessionLifetime;
}

/**
* Close previous session and create a new empty one.
*/
protected function recreateSession()
{
$sessionParams = $_SESSION;

$_SESSION = [];
session_unset();
session_destroy();

session_id(SessionWare::generateSessionId());

session_start();

foreach ($sessionParams as $param => $value) {
$_SESSION[$param] = $value;
}
}

/**
* Populate session with initial parameters if they don't exist.
*
Expand Down Expand Up @@ -416,7 +440,7 @@ protected function respondWithSessionCookie(ResponseInterface $response)
*
* @return string
*/
final public static function generateSessionId($length = 80)
final public static function generateSessionId($length = self::SESSION_ID_LENGTH)
{
return substr(
preg_replace('/[^a-zA-Z0-9-]+/', '', base64_encode(random_bytes((int) $length))),
Expand Down
48 changes: 36 additions & 12 deletions tests/Middleware/SessionWareTest.php
Expand Up @@ -56,6 +56,7 @@ public function setUp()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*
* @expectedException \RuntimeException
* @expectedExceptionMessageRegExp /^Session has already been started/
Expand All @@ -73,11 +74,18 @@ public function testSessionAlreadyStarted()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionTimeoutControlKey()
{
$middleware = new SessionWare(['name' => 'SessionWareSession', 'timeoutKey' => '__TIMEOUT__']);

$middleware($this->request, $this->response, $this->callback);

$limitTimeout = time() - SessionWare::SESSION_LIFETIME_EXTENDED;
$_SESSION['__TIMEOUT__'] = $limitTimeout;
session_write_close();

$sessionHolder = new \stdClass();
$middleware->addListener('pre.session_timeout', function ($sessionId) use ($sessionHolder) {
$sessionHolder->id = $sessionId;
Expand All @@ -91,19 +99,14 @@ public function testSessionTimeoutControlKey()

$middleware($this->request, $this->response, $this->callback);

$limitTimeout = time() - SessionWare::SESSION_LIFETIME_EXTENDED;
$_SESSION['__TIMEOUT__'] = $limitTimeout;
session_write_close();

$middleware($this->request, $this->response, $this->callback);

self::assertEquals(PHP_SESSION_ACTIVE, session_status());
self::assertTrue(array_key_exists('__TIMEOUT__', $_SESSION));
self::assertNotEquals($_SESSION['__TIMEOUT__'], $limitTimeout);
}

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage " " is not a valid session timeout
Expand All @@ -117,6 +120,7 @@ public function testSessionErrorTimeoutControlKey()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Session name must be a non empty string
Expand All @@ -130,6 +134,7 @@ public function testEmptySessionName()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionName()
{
Expand All @@ -145,38 +150,45 @@ public function testSessionName()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionIdFromFunction()
{
session_id('madeUpSessionId');
$sessionId = SessionWare::generateSessionId();

session_id($sessionId);

$middleware = new SessionWare(['name' => 'SessionWareSession']);

$middleware($this->request, $this->response, $this->callback);
/** @var Response $response */
$response = $middleware($this->request, $this->response, $this->callback);

self::assertEquals(PHP_SESSION_ACTIVE, session_status());
self::assertEquals('madeUpSessionId', session_id());
self::assertNotSame(strpos($response->getHeaderLine('Set-Cookie'), $sessionId), false);
}

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionIdFromRequest()
{
$request = ServerRequestFactory::fromGlobals(null, null, null, ['SessionWareSession' => 'madeUpSessionId']);
$sessionId = SessionWare::generateSessionId();

$request = ServerRequestFactory::fromGlobals(null, null, null, ['SessionWareSession' => $sessionId]);

$middleware = new SessionWare(['name' => 'SessionWareSession']);

/** @var Response $response */
$response = $middleware($request, $this->response, $this->callback);

self::assertEquals(PHP_SESSION_ACTIVE, session_status());
self::assertEquals('madeUpSessionId', session_id());
self::assertNotSame(strpos($response->getHeaderLine('Set-Cookie'), 'madeUpSessionId'), false);
self::assertNotSame(strpos($response->getHeaderLine('Set-Cookie'), $sessionId), false);
}

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testGeneratedSessionId()
{
Expand All @@ -189,6 +201,7 @@ public function testGeneratedSessionId()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionEmptySavePath()
{
Expand All @@ -202,6 +215,7 @@ public function testSessionEmptySavePath()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionSavePathFromFunction()
{
Expand All @@ -219,6 +233,7 @@ public function testSessionSavePathFromFunction()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionSavePathFromParameter()
{
Expand All @@ -234,6 +249,7 @@ public function testSessionSavePathFromParameter()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*
* @expectedException \RuntimeException
* @expectedExceptionMessageRegExp /^Failed to create session save path/
Expand All @@ -247,6 +263,7 @@ public function testSessionErrorSavePath()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionTimeoutDefault()
{
Expand All @@ -263,6 +280,7 @@ public function testSessionTimeoutDefault()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionTimeoutByCookieLifetime()
{
Expand All @@ -279,6 +297,7 @@ public function testSessionTimeoutByCookieLifetime()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionTimeoutByMaxLifetime()
{
Expand All @@ -295,6 +314,7 @@ public function testSessionTimeoutByMaxLifetime()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionTimeoutByParameter()
{
Expand All @@ -311,6 +331,7 @@ public function testSessionTimeoutByParameter()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Session lifetime must be at least 1
Expand All @@ -324,6 +345,7 @@ public function testSessionErrorTimeout()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionDefaultParams()
{
Expand All @@ -337,6 +359,7 @@ public function testSessionDefaultParams()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionCookieParams()
{
Expand Down Expand Up @@ -364,6 +387,7 @@ public function testSessionCookieParams()

/**
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
public function testSessionEndedCookieParams()
{
Expand Down

0 comments on commit d949bef

Please sign in to comment.