rfc:session-create-id

PHP RFC: Add session_create_id() function

Introduction

Session ID is created by session internal bin_to_readable() function. bin_to_readable() creates readable string from binary data depending. New session_create_id() uses bin_to_readable() to create user defined session ID string. Session ID may use 'a'-'z', 'A'-'Z', ',', '-'. Without session_create_id(), user has to implement their own bin_to_readable() in user land.

Proposal

Add session_create_id() function

string session_create_id([string $prefix])

NOTE: Prefix length is not considered as a part of session.sid_length. Session ID length became 'prefix length' + session.sid_length. Total length must be less than 256 chars.

session_create_id() will create new session ID by

  • Default internal function(php_session_create_id()) when session is NOT active.
  • Save handler defined session ID creation function when session is active.
    • Additionally, if session ID validation function is defined, collision is detected.

Note: User defined session ID creation function(handler) is not documented, but it's there. http://php.net/manual/en/book.session.php It's not documented because Object based save handler has non standard method name. i.e. create_sid() rather than createSid(). Rename will be proposed by different RFC.(Documented after this RFC is created) If you would like to see how it is used, please refer to phpt files.

Use case

Prefix session ID by user id. This could be useful to search active session IDs for a user.

<?php
function create_logged_in_session($uid) {
  // Mark obsolete session as obsolete. Timestamp based session management is mandatory.
  $_SESSION['OBSOLETE'] = time(); // Warn access to this session after a few minutes. Should be checked upon starting session.
 
  // Make sure old session does not have 'uid' in $_SESSION
  unset($_SESSION['uid']);
 
  // Session is active here. It is safe to generate new session ID while session is active,
  // because session ID collision is detected by session module if save handler implements session ID validation function.
  $sid = session_create_id($uid.'-');  
 
  // Save and close old session.
  session_commit(); 
 
  // use_strict_mode should be enabled by default for session security.
  // However, it must be disable to use custom session ID. 
  ini_set('session.use_strict_mode', false); 
 
  // Set your own session ID and start session.
  session_id($sid);
  // Collision is checked already in previous active session.
  session_start(); 
 
  // Error check
  if (session_id() !== $sid) {
    throw new Exception('Should not happen'); // Save handlers could do something cause this. This should never happen usually.
  }
 
  $_SESSION['uid'] = $uid;
}
 
function my_session_start() {
  // Should enable use_strict_mode for security reasons.
  ini_set('session.use_strict_mode', 1);
 
  session_start();
 
  // Check obsolete session
  if ($_SESSION['OBSOLETE'] < time() - 300) {
    throw new Exception('Obsolete session access. Possible security breach');
  } 
}
?>

If session data is stored in database, administrators are able to list active session per uid easily and efficiently.

Discussions

Should have str_bin2readable() instead of session_create_id()

Internal php_bin_to_readable() function that converts binary to chars has issues. - It's not designed to reversible. i.e. str_readable2bin() is not possible. It cannot be implemented usable way at least. - session_create_id() is mandatory function for session ID validation enabled user save handlers. There should be default session ID creation function. Otherwise, users have to implement by themselves and users could easily implement improper or broken session_create_id() function.

User land session_create_id() is easy

It would be easy with quick, dirty and insecure way. However, proper, precise and compatible session_create_id() is complex to do the exactly what proposed session_create_id() does.

function session_create_id(string $prefix)
{
    $encoded = base64_encode(random_bytes(ini_get('session.sid_length')*2));
    // Use same charset as PHP
    $sid = substr(rtrim(strtr($encoded, '+/', ',-'), '='), 0,
                          ini_get('session.sid_length');
    // This code does not consider session.hash_bits_per_character.
    // It must be handled also, but omitted here.
    // In addition, above code reduces session ID security by 
    // eliminating certain patterns of bits. i.e. Removing 
    // certain chars in BASE64 encoding string removes certain
    // patterns of bits. As a result, randomness of session ID
    // is spoiled.
 
    $sid .= $prefix;
 
    // Now validate SID so that it does not have collisions
    when session is active, connect to database and validate SID
      try to fetch sid
        if sid is there
          try again to generate SID few times
      if SID validation failed
         fatal error
      return safe SID
   when session is inactive
      return unvalidated SID
}

Something like above code is required to implement recommended user session save handlers currently.

The default 128 bits Session ID is large enough to ignore collisions

Brute force session ID hijack risk is described here. https://www.owasp.org/index.php/Insufficient_Session-ID_Length

The expected number of seconds required to guess a valid session
identifier is given by the equation:

(2^B+1)/(2*A*S)

Where:

B is the number of bits of entropy in the session identifier
A is the number of guesses an attacker can try each second
S is the number of valid session identifiers that are valid and
available to be guessed at any given time

It says

Now assume a 128 bit session identifier that provides 64 bits of entropy. With a very large web site, an attacker might try 10,000 guesses per second with 100,000 valid session identifiers available to be guessed. Given these assumptions, the expected time for an attacker to successfully guess a valid session identifier is greater than 292 years.

292 years may sound long enough. However, even though the document explicitly does not states “Session manager must validate session ID for possible collisions”, but it is clear it assumes session manager that validates session ID.

Let me paraphrase OWASP's document to show why.

Now assume a 128 bit session identifier that provides 64 bits of entropy. With a very large web site, legitimate users might create 10,000 new session ID per second (New and regenerated session) with 10,000,000 valid session identifiers available to be collided. Given these assumptions, the expected time of web system to successfully has collided identifier is greater than 2 years on average.

NOTE: It's about probability. Expectation is “on average” and could be much shorter.

Assumption for security should be pessimistic. OWASP makes pessimistic assumption for entropy in session ID, probably because proving “CSPRNG generates good quality of random bytes” is difficult.

10M active sessions are possible even with relatively small sites because there are users who use very long session ID life time for “auto login”. (This is not a recommended auto login implementation, though) 10K new session ID per second is possible for relatively small sites also because OWASP recommends session ID regeneration for every 15 minutes or less for certain usage.

In addition to above, current session management implementation does not support timestamp based session data management. i.e. https://wiki.php.net/rfc/precise_session_management This makes situation even worse.

Somebody wins lottery. Even 1 in millions/hundreds years could happen. IMHO, it is not reasonable to argue “Session ID collision very rare and cannot happen” or “Session ID is safe without collision detection, can ignore collisions”, and tell poor user “We do know it may happen, but you just had rare bad luck. Even though protection could be implemented, whatever consequences are your responsibility. It's the PHP way”.

If there are users who really do not want collision detection at all, they should do it by their own responsibility and risk. e.g.

if (session_status() == PHP_SESSION_ACTIVE) {
  session_commit();
}
ini_set('session.use_strict_mode', 0);
// NIST requires SHA2 or better hash for collision sensitive usage.
$new_sid = hash('sha512', random_bytes(128));
session_id($new_sid);
session_start()

Backward Incompatible Changes

None. Just a new function.

Proposed PHP Version(s)

PHP 7.2

Voting Choices

This project requires a 2/3 majority

Add session_create_id() frunction
Real name Yes No
bishop (bishop)  
diegopires (diegopires)  
ericsten (ericsten)  
lstrojny (lstrojny)  
mariano (mariano)  
mike (mike)  
ocramius (ocramius)  
stas (stas)  
yohgaki (yohgaki)  
Final result: 8 1
This poll has been closed.

Vote starts: 2016/08/10 - Vote ends: 2016/08/17 23:59:59 UTC

Patches and Tests

Implementation

After the project is implemented, this section should contain

  1. the version(s) it was merged to
    1. PHP 7.1 and master (Merged to 7.1 also by RM permission)
  2. a link to the git commit(s)
  3. a link to the PHP manual entry for the feature

References

Max/min length of session ID is defined by

Rejected Features

Keep this updated with features that were discussed on the mail lists.

rfc/session-create-id.txt · Last modified: 2016/09/01 03:37 by yohgaki