22

When I call session_start() or session_regenerate_id(), PHP generates what appears to be a random string for the session ID. What I want to know is, is it just a random sequence of characters, or is it like the uniqid() function?

Because if it's just random characters, couldn't you theoretically run into a conflict? If User A logged in and then User B logged in and, though highly unlikely, User B generated the same session ID, then User B would end up accessing User A's account.

Even if PHP checks to see if a session with the same ID already exists and, if so, regenerates an ID again... I don't think I want a system that EVER produces the same ID twice, even after garbage collection -- maybe I want to store a table of them and check against them for possible hijacking or whatever.

If it isn't unique, how should I go about enforcing uniqueness? I'd rather implement it using PHP configuration than in every script I make. Nice thing about PHP sessions is not worrying about the technical details behind the scenes.

M Miller
  • 5,364
  • 9
  • 43
  • 65
  • Related : http://stackoverflow.com/questions/138670/how-unique-is-the-php-session-id – LiamB Sep 21 '13 at 21:11
  • 1
    If you are this worried about security, then you should research a bit before you ask, as there are plenty of topics on the subject of PHP session security and hijacking. For secure environments, and things like PCI compliance, sessions in their original state are a no-no. – Mark Sep 21 '13 at 21:14
  • 1
    128 or 160 bits of effective randomness is more than enough to ensure that you'll never see a collision in your lifetime — and sessions don't last forever, so the problem is even more limited than that. – hobbs Sep 21 '13 at 21:26
  • Which isn't to say that you shouldn't ignore security, but making session IDs "truly unique" is nowhere *near* the best thing you can do to improve security. – hobbs Sep 21 '13 at 21:26
  • The point isn't uniqueness as a security constraint. I just want to store a history of session IDs. I don't know yet how many I intend to store before I start deleting old ones, but I don't want a chance of conflict in my database, which has a primary index on the session ID. – M Miller Sep 21 '13 at 21:33
  • I don't see how this is a duplicate. This question is asking about what algorithm PHP used, and the other question is about how unique/random is the algorithm. – Kayla Aug 31 '14 at 01:51

2 Answers2

56

If you want to know how PHP generates a session ID by default check out the source code on Github. It is certainly not random and is based on a hash (default: md5) of these ingredients (see line 310 of code snippet):

  1. IP address of the client
  2. Current time
  3. PHP Linear Congruence Generator - a pseudo random number generator (PRNG)
  4. OS-specific random source - if the OS has a random source available (e.g. /dev/urandom)

If the OS has a random source available then strength of the generated ID for the purpose of being a session ID is high (/dev/urandom and other OS random sources are (usually) cryptographically secure PRNGs). If however it does not then it is satisfactory.

The goal with session identification generation is to:

  1. minimise the probability of generating two session IDs with the same value
  2. make it very challenging computationally to generate random keys and hit an in use one.

This is achieved by PHP's approach to session generation.

You cannot absolutely guarantee uniqueness, but the probabilities are so low of hitting the same hash twice that it is, generally speaking, not worth worrying about.

neubert
  • 15,947
  • 24
  • 120
  • 212
GordyD
  • 5,063
  • 25
  • 29
  • 1
    Thanks for the complete summary. I've decided that, given a relatively short lifetime for a session, it's really nothing to worry about. Since I haven't determined when/how often I will truncate my database table, I'm going to just add a timestamp to the primary index to make a query error impossible. Also, I'm going to up the entropy and set the hash function to my favorite, Whirlpool. Thanks again! – M Miller Sep 21 '13 at 21:45
  • No worries. And yea you can change the hashing function to sha1 or any other in this list (http://www.php.net/manual/en/function.hash-algos.php) - including whirlpool! – GordyD Sep 21 '13 at 21:52
  • 2
    Actually, we just figured out the probabilities of generating the same session id on 2 different servers is quite high when they are running behind a reverse proxy and their clocks are synced. In this case, the REMOTE_ADDR is the same for the 2 servers. And, as the PRNG seed is based on the current second, we are getting in average 2 identical session ids per on a total of 150 on each server! I would strongly suggest to always make sure the session entropy is set to something big enough (just in case) and use a shared session handler like memcached or redis. – Benoît Vidis Nov 16 '16 at 08:49
  • @BenoîtVidis On a unique server I hope PHP checks for uniqueness anyway ; as for 2+ servers, it's maybe better to generate one's own algo (that includes a server number in the string to be hashed) and force PHP to use that hash as a session ID, to greatly mitigate the risk of having *déjà-vus* :) – Déjà vu Nov 29 '21 at 05:04
9

Here is the code that generates the id: Session.c

Specifically the php_session_create_id function:

PHPAPI char *php_session_create_id(PS_CREATE_SID_ARGS) /* {{{ */
{
    PHP_MD5_CTX md5_context;
    PHP_SHA1_CTX sha1_context;
#if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
    void *hash_context = NULL;
#endif
    unsigned char *digest;
    int digest_len;
    int j;
    char *buf, *outid;
    struct timeval tv;
    zval **array;
    zval **token;
    char *remote_addr = NULL;

    gettimeofday(&tv, NULL);

    if (zend_hash_find(&EG(symbol_table), "_SERVER", sizeof("_SERVER"), (void **) &array) == SUCCESS &&
        Z_TYPE_PP(array) == IS_ARRAY &&
        zend_hash_find(Z_ARRVAL_PP(array), "REMOTE_ADDR", sizeof("REMOTE_ADDR"), (void **) &token) == SUCCESS
    ) {
        remote_addr = Z_STRVAL_PP(token);
    }

    /* maximum 15+19+19+10 bytes */
    spprintf(&buf, 0, "%.15s%ld%ld%0.8F", remote_addr ? remote_addr : "", tv.tv_sec, (long int)tv.tv_usec, php_combined_lcg(TSRMLS_C) * 10);

    switch (PS(hash_func)) {
        case PS_HASH_FUNC_MD5:
            PHP_MD5Init(&md5_context);
            PHP_MD5Update(&md5_context, (unsigned char *) buf, strlen(buf));
            digest_len = 16;
            break;
        case PS_HASH_FUNC_SHA1:
            PHP_SHA1Init(&sha1_context);
            PHP_SHA1Update(&sha1_context, (unsigned char *) buf, strlen(buf));
            digest_len = 20;
            break;
#if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
        case PS_HASH_FUNC_OTHER:
            if (!PS(hash_ops)) {
                php_error_docref(NULL TSRMLS_CC, E_ERROR, "Invalid session hash function");
                efree(buf);
                return NULL;
            }

            hash_context = emalloc(PS(hash_ops)->context_size);
            PS(hash_ops)->hash_init(hash_context);
            PS(hash_ops)->hash_update(hash_context, (unsigned char *) buf, strlen(buf));
            digest_len = PS(hash_ops)->digest_size;
            break;
#endif /* HAVE_HASH_EXT */
        default:
            php_error_docref(NULL TSRMLS_CC, E_ERROR, "Invalid session hash function");
            efree(buf);
            return NULL;
    }
    efree(buf);

    if (PS(entropy_length) > 0) {
#ifdef PHP_WIN32
        unsigned char rbuf[2048];
        size_t toread = PS(entropy_length);

        if (php_win32_get_random_bytes(rbuf, MIN(toread, sizeof(rbuf))) == SUCCESS){

            switch (PS(hash_func)) {
                case PS_HASH_FUNC_MD5:
                    PHP_MD5Update(&md5_context, rbuf, toread);
                    break;
                case PS_HASH_FUNC_SHA1:
                    PHP_SHA1Update(&sha1_context, rbuf, toread);
                    break;
# if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
                case PS_HASH_FUNC_OTHER:
                    PS(hash_ops)->hash_update(hash_context, rbuf, toread);
                    break;
# endif /* HAVE_HASH_EXT */
            }
        }
#else
        int fd;

        fd = VCWD_OPEN(PS(entropy_file), O_RDONLY);
        if (fd >= 0) {
            unsigned char rbuf[2048];
            int n;
            int to_read = PS(entropy_length);

            while (to_read > 0) {
                n = read(fd, rbuf, MIN(to_read, sizeof(rbuf)));
                if (n <= 0) break;

                switch (PS(hash_func)) {
                    case PS_HASH_FUNC_MD5:
                        PHP_MD5Update(&md5_context, rbuf, n);
                        break;
                    case PS_HASH_FUNC_SHA1:
                        PHP_SHA1Update(&sha1_context, rbuf, n);
                        break;
#if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
                    case PS_HASH_FUNC_OTHER:
                        PS(hash_ops)->hash_update(hash_context, rbuf, n);
                        break;
#endif /* HAVE_HASH_EXT */
                }
                to_read -= n;
            }
            close(fd);
        }
#endif
    }

    digest = emalloc(digest_len + 1);
    switch (PS(hash_func)) {
        case PS_HASH_FUNC_MD5:
            PHP_MD5Final(digest, &md5_context);
            break;
        case PS_HASH_FUNC_SHA1:
            PHP_SHA1Final(digest, &sha1_context);
            break;
#if defined(HAVE_HASH_EXT) && !defined(COMPILE_DL_HASH)
        case PS_HASH_FUNC_OTHER:
            PS(hash_ops)->hash_final(digest, hash_context);
            efree(hash_context);
            break;
#endif /* HAVE_HASH_EXT */
    }

    if (PS(hash_bits_per_character) < 4
            || PS(hash_bits_per_character) > 6) {
        PS(hash_bits_per_character) = 4;

        php_error_docref(NULL TSRMLS_CC, E_WARNING, "The ini setting hash_bits_per_character is out of range (should be 4, 5, or 6) - using 4 for now");
    }

    outid = emalloc((size_t)((digest_len + 2) * ((8.0f / PS(hash_bits_per_character)) + 0.5)));
    j = (int) (bin_to_readable((char *)digest, digest_len, outid, (char)PS(hash_bits_per_character)) - outid);
    efree(digest);

    if (newlen) {
        *newlen = j;
    }

    return outid;
}

As you can see the actual id is a hash of a mixture of things, like the time of day. So there is a possibility of running into a conflict, however, it has a very low possibility. So much so, it is not worth worrying about unless you have lots of concurrent users.

However, if you really are worried you can increase the entropy by setting a different hash algorithm session.hash_function

As far as monitoring active sessions, this question covers it well Is it possible to see active sessions using php?

If you are using a single instance of php on a single machine, then it actually has a built in session manager that checks whether an id already exists before assigning it. However, if you are running multiple instances or multiple machines it has no way of knowing what ids have been assigned by other machines.

Community
  • 1
  • 1
1321941
  • 2,139
  • 5
  • 26
  • 49