rfc:precise_session_management

This is an old revision of the document!


PHP RFC: Precise Session Management

Introduction

This proposal fixes session management design issue. Session ID management requires more precise management. Otherwise,

  • Outstanding session ID is lost randomly.
  • Session ID cookie becomes empty randomly.
  • Increase risks of stolen session.

Keeping HTTP session as secure as possible is what the session manager's task. Session manager can improve HTTP session security without user code modification while keeping compatibility with existing applications. Please note that this RFC is for session manager behavior.

TL;DR;

This RFC solves session_regenerate_id() problems and adds session abuse mitigation.

  • Solve random lost session.
  • Solve session ID being null string randomly.
  • Add stolen session access mitigation.
    • Disable access to obsolete(old/expired) session data.
    • Allow to detect abuse of stolen session. (False positive could occur, but it should be rare.)
    • Disallow keep stolen session forever
      • Add automatic session ID regeneration.
      • Disallow uninitialized session ID initialization by default.

In other words, above problems happen and PHP does not have the mitigation currently.

Followings will be added/changed to achieve above.

  • Add session module internal data to stored session data.
  • Keep obsolete(old/expired) session alive for certain period. (Consider obsolete session data is valid for short period.)
  • Store old session IDs. (Old session IDs may be required for CSRF protection, etc)
  • Manage session expiration time stricter manner.
  • Raise error for obsolete(old/expired) session data access.
  • Regenerate session ID for certain period.
  • Use more secure INI. Use strict mode by default. (session.use_strict_mode=1) and so on.

session_regenerate_id() problems

session_regenerate_id() is used to generate new session ID. It's better to delete old session data to reduce risk of session hijack. However, session_regenerate_id() leave old session data by default currently. (i.e. session_regenerate_id(FALSE) is the default) Old session data is active and usable until GC.

Old session is left active for reliable session ID regeneration. There are many reasons why old session is left. Examples are:

  • Browsers connect to web server with multiple connections.
  • Mobile network may loose radio, may have hand over, etc.
  • Large network providers have multiple gateways for off loading traffic and packets may arrive out of order.

For reliable session ID regeneration, only short TTL for old session (few seconds for wired connections, few minutes for mobile connection) is enough for stable HTTP session management.

Session ID can be stolen various ways. Therefore, session ID should be renewed periodically to reduce risk of stolen sessions.

Leaving old session opens window to attacker widely:

  1. Old session lives long term and never expires if there is access to it. i.e. Attacker may abuse stolen session forever.
  2. There is no mechanism to detect possibles attack even if session manager may detect attacks.

Counter measure for session hijack: Requirement

  1. Session ID regeneration must be reliable.
  2. Make sure old session is deactivated/deleted after certain period.
  3. Raise error/exception for invalid access. (Raise error for should be deleted session access)

Problem of immediate old session deletion:

  1. Make session ID regeneration unreliable. i.e. session_regenerate_id(TRUE) randomly destroy session data and/or browsers sent empty session ID. See referenced bug reports. (Unacceptable)
  2. Remove alarm for possible attacks. (No detection = Insecure)
  3. Cannot prevent race condition.

“Make sure old session is deleted certain period” and “Raise error/exception for invalid access” provides much better security than current way (leave obsolete session invalidation to probability based GC) or immediate deletion.

Errors for accessing invalid session may be raised for either legitimate user or attacker. If error is raised for legitimate user, legitimate user could know they are under attack. (Possibly network is dangerous or app has vulnerability or their network connection was too bad) If error is raised for attacker, attacker could know their illegal access might be caught.

Session storage which does not support serializable transaction cannot prevent race condition on immediate deletion. For example, files save handler locks session data file and if other request try to read it, it waits unlock. If session_regenerate_id(TRUE) is called, file is unlocked and unlinked. Later request end up with empty session data that results in loss of $_SESSION content. RBDMS based storage can issue transaction error and may ask browser to resend request, but it is difficult for storage supports only simple lock.

Session Expiration Problems

Current session expiration is not precise as it depends on probability for deletion, 1/1000 probability by default. session_regenerate_id(FALSE) is affected directly by this. Obsolete(old) session data is left for GC.

Since session module should not delete old session data immediately, session module leave old session removal to GC by default. As a result, invalid old session may alive as long as it is accessed.

Session expiration should be more precise. Obsolete session data must be removed few seconds to few minutes later.

Risks of stolen session

Stealing session ID is easy regardless of HTTPS. Attacker can set up fake router by ARP spoofing. Most networks do not have ARP spoofing prevention, even detection. For HTTP, attacker can view session ID simply. For HTTPS, attacker can set up transparent HTTPS stripping proxy and steal session ID. Most users do not care much if they are connecting via HTTPS or not.

Above attack can be done by existing tools. i.e. Script kiddies' task.

If you are curious, search YouTube or net.

There are many enterprise networks that sniff TLS/SSL traffic. If psychical access to device is possible, stealing session ID is trivial also.

This is known design issue for a long time

Even if there is only recent bug report for this, this bug is known for a long time.

session_regenerate_id() worked as session_regenerate_id(TRUE) (Remove old session data immediately) when it was made. session_regenerate_id(TRUE) caused problems like above bug reports and the default behavior was changed to session_regenerate_id(FALSE). (IIRC, session_regenerate_id(FALSE) behavior was added later and became the default.)

Proposal

Based on the fact that

  • Old session data cannot be deleted immediately.
  • Obsolete old session data must be removed by reliable manner.

This RFC proposes followings

  • Add transparent Session module internal array data $_SESSION['__PHP_SESSION__'] to keep access time stamp, etc. Note: This array could be used to store data for automatic CSRF protection in the future also.
  • Add session.ttl INI (Default 1800 seconds) Take over the purpose of session.gc_maxlifetime.
  • Add session.ttl_update INI (Default 300 seconds. Last access time stamp update frequency)
  • Add session.ttl_destroy INI (Default 300 seconds. Session destroy TTL)
  • Add session.regenerate_id INI (Default 18 hours in seconds. Time to automatic session ID regeneration)
  • Add session.num_sids INI (Default 8. Number of stored session IDs in session internal data)
  • Add session_info() function that returns internal data.
  • Add session_gc() function for periodic GC task.
  • Remove immediate old session data deletion from session_regenerate_id().
  • Make immediate session destroy by session_destroy() optional.
  • Reject null string session ID. (Already fixed as bug fix)
  • Use stronger/more secure settings. Use strict mode by default (disallow stealing session forever). Use httponly cookie by default. Use SHA1 as hash (and use 5 bits for hashed value string for better compatibility)

How session_regenerate_id() will work

NOTE: This is PHP pseudo code, but it will be implemented exactly as it is.

Session manager sets following data when there is session data should be deleted. i.e. session_regenerate_id() is called.

Obsolete session data has NEW_SID and TTL upto session.ttl_destroy. UPDATED timestamp is used to check TTL.

  $_SESSION['__PHP_SESSION__']['NEW_SID'] = <new session ID>;

New session data has TTL, TTL_UPDATE and CREATED.

  $_SESSION['__PHP_SESSION__']['CREATED'] = time();
  $_SESSION['__PHP_SESSION__']['UPDATED'] = time();

Under normal session usage, $_SESSION['__PHP_SESSION__'] is checked/used as follows

  // Check if session ID should be regenerated
  if (ini_get('session.regenerate_id')
      && isset($_SESSION['__PHP_SESSION__']['CREATED'])
      && $_SESSION['__PHP_SESSION__']['CREATED'] + ini_get('session.regenerate_id') < time()) {
      session_regenerate_id();
  }
  // Check obsolete session and destroy.
  if (isset($_SESSION['__PHP_SESSION__']['NEW_SID'])) {
    // Must not update obsolete session TTL. 
    if ($_SESSION['__PHP_SESSION__']['UPDATED'] - time() < -60
        && !isset($_SESSION['__PHP_SESSION__']['NEW_SID_SENT']) {
      // Resend new session ID once. This will reduce chance of client race and lost session by unstable network to acceptable level.
      $_SESSION['__PHP_SESSION__']['NEW_SID_SENT'] = time();
    }
    if ($_SESSION['__PHP_SESSION__']['UPDATED'] + ini_get('session.ttl') < time()) {
      // Remove session data
      // Assign newly created session ID
      // Raise warning for obsolete session data access
    }
  } else {
    if ($_SESSION['__PHP_SESSION__']['UPDATED'] + ini_get('session.ttl') < time()) {
      // Remove session data
      // Assign newly created session ID
    }
    // TTL is updated by session.ttl_update for lazy_write to work.
    else if ($_SESSION['__PHP_SESSION__']['UPDATED'] + ini_get('session.ttl_update') < time()) {
      $_SESSION['__PHP_SESSION__']['UPDATED'] = time();    
    }
  }

User will not see $_SESSION['__PHP_SESSION__'] array as it is removed/added upon session data serialization internally in session module. Use may get $_SESSION['__PHP_SESSION__'] contents via session_info() function.

When session_regenerate_id()/session_destroy() is called, session module keep old session up to ini_get('session.ttl_destroy').

Users may add $_SESSION['__PHP_SESSION__']. When this is happened, session module raise E_WARNING and replace with the session internal data.

Why session.ttl_destroy default is 300 seconds and configurable

Session data may be lost when network connection is unstable. For example, when user enter elevator or subway, connection can be lost in a way that session data is lost. 300 seconds would be enough for most elevators. However, it may not be enough for subways. PHP developer may require longer TTL for better stability.

Some PHP developers may want to be more stric/shorter TTL even if it could result in lost session on occasions. They may set 30 seconds TTL which would be long enough for stable connection in most cases.

Why session.regenerate_id default is 18 hours

Shorter is better for stolen session abuse mitigation. However, many apps rely on “fixed session ID”. Therefore, the default is set to rather long period. 18 hours is probably good enough for daily use.

Why this is secure than now

Currently, users must call session_regenerate_id() without destroy flag to have stable session. Therefore, old session data is valid as long as it is accessed even if it should be discarded as invalid session. Attackers can take advantage of this behavior to keep stolen session forever.

Since current session depends on probability based GC, low traffic site may keep obsolete session data for long period.

$_SESSION['__PHP_SESSION__'] data definition

  • $_SESSION['__PHP_SESSION__']['CREATED'] : Session data creation time. Used for automatic session ID regeneration.
  • $_SESSION['__PHP_SESSION__']['UPDATED'] : Update time. UNIX time. Used for TTL management.
  • $_SESSION['__PHP_SESSION__']['NEW_SID'] : New session ID generated by session_regenerate_id(). If this is set, it means this session is obsolete. New session ID string is set for regenerate ID. NULL is set for destroyed.
  • $_SESSION['__PHP_SESSION__']['NEW_SID_SENT'] : Set if NEW_SID is resent to client. Used to prevent browser cookie storage race condition. UNIX time.
  • $_SESSION['__PHP_SESSION__']['SIDS'] : Stores previously used session IDs up to 8 IDs. Number of stored old session IDs are defined by session.num_sids INI.

Change session_destroy()

Accept long parameter for session data removal duration.

bool session_gc([long $removal_duration = ini_get('session.ttl_destroy')]) 

Add session_info()

Returns session internal data array for session management.

Example return value.

array(3) {
  ["CREATED"]=>
  int(%d)
  ["UPDATED"]=>
  int(%d)
  ["SIDS"]=>
  array(9) {
    [2]=>
    string(32) "%s"
    [3]=>
    string(32) "%s"
    [4]=>
    string(32) "%s"
    [5]=>
    string(32) "%s"
    [6]=>
    string(32) "%s"
    [7]=>
    string(32) "%s"
    [8]=>
    string(32) "%s"
    [9]=>
    string(32) "%s"
    [10]=>
    string(32) "%s"
  }
}

Add session_gc()

Probability based expiration for obsolete sessions is no longer required with TTL time stamp. However, garbage will be left. Therefore, there should be GC API for cron task for instance.

int session_gc(void) // Returns number of deleted session data

Add session_info()

Session ID may be used for security purposes such as CSRF protection, input validation, etc. When session ID is regenerated, these protections may not work. This function returns array of session IDs where smallest numeric key is the oldest and stores up to 8 IDs. Number of stored SIDs are configurable by session.num_sids INI.

array session_info(void) // Returns session internal data array 

Add session.ttl

Even though session.gc_maxlifetime could be used for TLL, it is no longer proper INI for session expiration control. There should be proper INI for TTL value. session.gc_maxlifetime is there for systems that cannot perform periodic GC by session_gc() function.

Backward Incompatible Changes

  • If user script has __PHP_SESSION__ key in $_SESSION, it may break application.
  • Raised errors for stricter session management may break application.
  • Direct reading/decoding session data will see the __PHP_SESSION__ data. There are 3rd party libraries that read/write PHP encoded session data. New key may break apps use these libraries.
  • Applications/Framework's tests may be broken by logic change.
  • If old/new PHP is mixed, old PHP will see new PHP's internal data structure.
  • Although it is not recommended, user may rely on session ID for CSRF protections. Automatic session ID regeneration breaks these applications. NOTE: Automatic session ID regeneration may be disabled. (session.regenerate_id=0)

Proposed PHP Version(s)

PHP 7.1

SAPIs Impacted

  • None

Impact to Existing Extensions

  • Session
  • Modules have session save handlers, session serialize handlers.

New Constants

  • None

php.ini Defaults

If there are any php.ini settings then list:

  • hardcoded default values
  • php.ini-development values
  • php.ini-production values

New

  • “session.ttl = 1800” for all. (1800 seconds. Replaces session.gc_maxlifetime. INI_ALL)
  • “session.ttl_update = 300” for all. (300 seconds. TTL update frequency. If TTL is updated for every request, lazy_write won't work. INI_ALL)
  • “session.ttl_destroy = 300” for all. (300 seconds. TTL value for removing obsolete session. INI_ALL)
  • “session.regenerate_id = 64800” for all. (64800 seconds (18 hours). Time to automatic session ID regeneration. 0 for disable it. INI_ALL)

Existing

  • “session.gc_divisor = 5000” for all. Currently, 100 hardcoded, 1000 for development/production. Changed since less frequent GC is required with new session management.
  • “session.gc_maxlifetime = 3600” for all. Currently 1440. This value should be larger than “session.ttl”.
  • “session.use_strict_mode = 1” for all. Currently 0. Initializing session by uninitialized session ID allows attackers to abuse.
  • “session.hash_function = 1” for all. Compiled default is 0 currently. INIs' are 1.
  • “session.hash_bits_per_characters = 5” for all. Compiled default is 4 currently. INIs' are 5.
  • “session.cookie_httponly = 1” for all. Currently 0 for all.

Open Issues

Unaffected PHP Functionality

Other than session management, there is no affected functionality.

Future Scope

Fully automatic/site wide CSRF protection may be introduced with $_SESSION['__PHP_SESSION__'] and rewrite var feature.

Vote

Requires 50%+ vote is required.

Precise Session Data Management
Real name Yes No
ab (ab)  
ajf (ajf)  
danack (danack)  
derick (derick)  
diegopires (diegopires)  
dm (dm)  
galvao (galvao)  
guilhermeblanco (guilhermeblanco)  
ircmaxell (ircmaxell)  
krakjoe (krakjoe)  
levim (levim)  
mcmic (mcmic)  
mightyuhu (mightyuhu)  
mike (mike)  
nikic (nikic)  
pajoye (pajoye)  
patrickallaert (patrickallaert)  
pauloelr (pauloelr)  
peehaa (peehaa)  
pierrick (pierrick)  
sammyk (sammyk)  
stas (stas)  
trowski (trowski)  
tyrael (tyrael)  
yohgaki (yohgaki)  
zeev (zeev)  
Final result: 15 11
This poll has been closed.

Patches and Tests

References

ChangeLog

  • 2016/01/25 - Remove TTL and TTL_UPDATE from PHP_SESSION.
  • 2016/01/23 - Remove session_ids() in favor of session_info()
  • 2016/01/22 - Added PR. Modified RFC according to PR.
  • 2016/01/14 - Added session storage race condition.
  • 2015/12/30 - Added session_ids() function.
  • 2015/12/24 - Include automatic session ID regeneration.
  • 2015/12/24 - Added TL;DR; section.
  • 2015/12/23 - Update RFC to use <nowiki>$_SESSION['PHP_SESSION'] array.
  • 2015/12/18 - Update RFC to use <nowiki>$_SESSION['SESSION_INTERNAL'] array.
  • 2015/03/21 - Added new session ID handling.
  • 2015/03/20 - Change INI directive name.
  • 2014/03/19 - Add exception option as Stas suggested.
  • 2014/03/18 - Change RFC to propose time stamping.
  • 2013/10/30 - Added details and message option.
  • 2013/10/29 - Created RFC
rfc/precise_session_management.1453806448.txt.gz · Last modified: 2017/09/22 13:28 (external edit)