PHP Session File Hacks

If you want to learn best tricks and tips, there's only one way to do it... at least only one way that I know of. Here are some notes I created while learning about the intricacies of php sessions, it's all in the code.

[Session]
; Handler used to store/retrieve data.
session.save_handler = files

Argument passed to save_handler. In the case of files, this is the path where data files are stored. As of PHP 4.0.1, you can define the path as:

session.save_path = "N;/path"

where N is an integer. Instead of storing all the session files in /path, what this will do is use subdirectories N-levels deep, and store the session data in those directories. This is useful if you or your OS have problems with lots of files in one directory, and is a more efficient layout for servers that handle lots of sessions.

;
; NOTE 1: PHP will not create this directory structure automatically.
;         You can use the script in the ext/session dir for that purpose.
; NOTE 2: See the section on garbage collection below if you choose to
;         use subdirectories for session storage
;
; The file storage module creates files using mode 600 by default.
; You can change that by using
;
;     session.save_path = "N;MODE;/path"
;
; where MODE is the octal representation of the mode. Note that this
; does not overwrite the process's umask.
;session.save_path = "/tmp"

session.c

/* {{{ PHP_INI
 */
PHP_INI_BEGIN()
        STD_PHP_INI_BOOLEAN("session.bug_compat_42",    "1",         PHP_INI_ALL, OnUpdateBool,   bug_compat,         php_ps_globals,    ps_globals)
        STD_PHP_INI_BOOLEAN("session.bug_compat_warn",  "1",         PHP_INI_ALL, OnUpdateBool,   bug_compat_warn,    php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.save_path",          "",          PHP_INI_ALL, OnUpdateSaveDir,save_path,          php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.name",               "PHPSESSID", PHP_INI_ALL, OnUpdateString, session_name,       php_ps_globals,    ps_globals)
        PHP_INI_ENTRY("session.save_handler",           "files",     PHP_INI_ALL, OnUpdateSaveHandler)
        STD_PHP_INI_BOOLEAN("session.auto_start",       "0",         PHP_INI_ALL, OnUpdateBool,   auto_start,         php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.gc_probability",     "1",         PHP_INI_ALL, OnUpdateLong,    gc_probability,     php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.gc_divisor",         "100",       PHP_INI_ALL, OnUpdateLong,    gc_divisor,        php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.gc_maxlifetime",     "1440",      PHP_INI_ALL, OnUpdateLong,    gc_maxlifetime,     php_ps_globals,    ps_globals)
        PHP_INI_ENTRY("session.serialize_handler",      "php",       PHP_INI_ALL, OnUpdateSerializer)
        STD_PHP_INI_ENTRY("session.cookie_lifetime",    "0",         PHP_INI_ALL, OnUpdateLong,    cookie_lifetime,    php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.cookie_path",        "/",         PHP_INI_ALL, OnUpdateString, cookie_path,        php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.cookie_domain",      "",          PHP_INI_ALL, OnUpdateString, cookie_domain,      php_ps_globals,    ps_globals)
        STD_PHP_INI_BOOLEAN("session.cookie_secure",    "",          PHP_INI_ALL, OnUpdateBool,   cookie_secure,      php_ps_globals,    ps_globals)
        STD_PHP_INI_BOOLEAN("session.cookie_httponly",  "",          PHP_INI_ALL, OnUpdateBool,   cookie_httponly,    php_ps_globals,    ps_globals)
        STD_PHP_INI_BOOLEAN("session.use_cookies",      "1",         PHP_INI_ALL, OnUpdateBool,   use_cookies,        php_ps_globals,    ps_globals)
        STD_PHP_INI_BOOLEAN("session.use_only_cookies", "0",         PHP_INI_ALL, OnUpdateBool,   use_only_cookies,   php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.referer_check",      "",          PHP_INI_ALL, OnUpdateString, extern_referer_chk, php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.entropy_file",       "",          PHP_INI_ALL, OnUpdateString, entropy_file,       php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.entropy_length",     "0",         PHP_INI_ALL, OnUpdateLong,    entropy_length,     php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.cache_limiter",      "nocache",   PHP_INI_ALL, OnUpdateString, cache_limiter,      php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.cache_expire",       "180",       PHP_INI_ALL, OnUpdateLong,    cache_expire,       php_ps_globals,    ps_globals)
        PHP_INI_ENTRY("session.use_trans_sid",          "0",         PHP_INI_ALL, OnUpdateTransSid)
        STD_PHP_INI_ENTRY("session.hash_function",      "0",         PHP_INI_ALL, OnUpdateLong,    hash_func,          php_ps_globals,    ps_globals)
        STD_PHP_INI_ENTRY("session.hash_bits_per_character",      "4",         PHP_INI_ALL, OnUpdateLong,    hash_bits_per_character,          php_ps_globals,    ps_globals)

        /* Commented out until future discussion */
        /* PHP_INI_ENTRY("session.encode_sources", "globals,track", PHP_INI_ALL, NULL) */
PHP_INI_END()
/* }}} */

Session Errors

The session id contains illegal characters, valid characters are a-z, A-Z, 0-9 and '-,'
fcntl(%d, F_SETFD, FD_CLOEXEC) failed: %s (%d)
open(%s, O_RDWR) failed: %s (%d)
ps_files_cleanup_dir: opendir(%s) failed: %s (%d)
read failed: %s (%d)
read returned less bytes than requested
write failed: %s (%d)
write wrote less bytes than requested
mm_malloc failed, avail %d, err %s
cannot allocate new data segment
Skipping numeric key %ld.
A session is active. You cannot change the session module's ini settings at this time.
Cannot find save handler %s
Cannot find serialization handler %s
Unknown session.serialize_handler. Failed to encode session object.
Cannot encode non-existent session.
Unknown session.serialize_handler. Failed to decode session object.
Failed to decode session object. Session has been destroyed.
Invalid session hash function
The ini setting hash_bits_per_character is out of range (should be 4, 5, or 6) - using 4 for now
No storage module chosen - failed to initialize session.
Failed to initialize storage module: %s (path: %s)
The session bug compatibility code will not
Your script possibly relies on a session side-effect which existed until PHP 4.2.3. Please be advised that the session extension does not consider global variables as a source of data, unless register_globals is enabled. You can disable this functionality and this warning by setting session.bug_compat_42 or session.bug_compat_warn to off, respectively.
Failed to write session data (%s). Please
Cannot send session cache limiter - headers already sent (output started at %s:%d)
Cannot send session cache limiter - headers already sent
Cannot send session cookie - headers already sent by (output started at %s:%d)
Cannot send session cookie - headers already sent
Cannot find save handler %s
Cannot find unknown save handler
purged %d expired session objects
Trying to destroy uninitialized session
Session object destruction failed
Cannot find named PHP session module (%s)
Argument %d is not a valid callback
Cannot regenerate session id - headers already sent
Session object destruction failed
PS_GC_FUNC(files)
{
        PS_FILES_DATA;

        /* we don't perform any cleanup, if dirdepth is larger than 0.
           we return SUCCESS, since all cleanup should be handled by
           an external entity (i.e. find -ctime x | xargs rm) */

        if (data->dirdepth == 0) {
                *nrdels = ps_files_cleanup_dir(data->basedir, maxlifetime TSRMLS_CC);
        }

        return SUCCESS;
}

mod_files.c

/* If you change the logic here, please also update the error message in
 * ps_files_open() appropriately */
static int ps_files_valid_key(const char *key)
{
        size_t len;
        const char *p;
        char c;
        int ret = 1;

        for (p = key; (c = *p); p++) {
                /* valid characters are a..z,A..Z,0..9 */
                if (!((c >= 'a' && c <= 'z')
                                || (c >= 'A' && c <= 'Z')
                                || (c >= '0' && c <= '9')
                                || c == ','
                                || c == '-')) {
                        ret = 0;
                        break;
                }
        }

        len = p - key;

        if (len == 0) {
                ret = 0;
        }

        return ret;
}
static int ps_files_cleanup_dir(const char *dirname, int maxlifetime TSRMLS_DC)
{
        DIR *dir;
        char dentry[sizeof(struct dirent) + MAXPATHLEN];
        struct dirent *entry = (struct dirent *) &dentry;
        struct stat sbuf;
        char buf[MAXPATHLEN];
        time_t now;
        int nrdels = 0;
        size_t dirname_len;

        dir = opendir(dirname);
        if (!dir) {
                php_error_docref(NULL TSRMLS_CC, E_NOTICE, "ps_files_cleanup_dir: opendir(%s) failed: %s (%d)", dirname, strerror(errno), errno);
                return (0);
        }

        time(&now);

        dirname_len = strlen(dirname);

        /* Prepare buffer (dirname never changes) */
        memcpy(buf, dirname, dirname_len);
        buf[dirname_len] = PHP_DIR_SEPARATOR;

        while (php_readdir_r(dir, (struct dirent *) dentry, &entry) == 0 && entry) {
                /* does the file start with our prefix? */
                if (!strncmp(entry->d_name, FILE_PREFIX, sizeof(FILE_PREFIX) - 1)) {
                        size_t entry_len = strlen(entry->d_name);

                        /* does it fit into our buffer? */
                        if (entry_len + dirname_len + 2 < MAXPATHLEN) {
                                /* create the full path.. */
                                memcpy(buf + dirname_len + 1, entry->d_name, entry_len);

                                /* NUL terminate it and */
                                buf[dirname_len + entry_len + 1] = '�';

                                /* check whether its last access was more than maxlifet ago */
                                if (VCWD_STAT(buf, &sbuf) == 0 &&
#ifdef NETWARE
                                                (now - sbuf.st_mtime.tv_sec) > maxlifetime) {
#else
                                                (now - sbuf.st_mtime) > maxlifetime) {
#endif
                                        VCWD_UNLINK(buf);
                                        nrdels++;
                                }
                        }
                }
        }

        closedir(dir);

        return (nrdels);
}

ext/session/mod_files.c

#define PS_FILES_DATA ps_files *data = PS_GET_MOD_DATA()

PS_OPEN_FUNC(files)
{
        ps_files *data;
        const char *p, *last;
        const char *argv[3];
        int argc = 0;
        size_t dirdepth = 0;
        int filemode = 0600;

        if (*save_path == '�') {
                /* if save path is an empty string, determine the temporary dir */
                save_path = php_get_temporary_directory();

                if (strcmp(save_path, "/tmp")) {
                        if (PG(safe_mode) && (!php_checkuid(save_path, NULL, CHECKUID_CHECK_FILE_AND_DIR))) {
                                return FAILURE;
                        }
                        if (php_check_open_basedir(save_path TSRMLS_CC)) {
                                return FAILURE;
                        }
                }
        }

        /* split up input parameter */
        last = save_path;
        p = strchr(save_path, ';');
        while (p) {
                argv[argc++] = last;
                last = ++p;
                p = strchr(p, ';');
                if (argc > 2) break;
        }
        argv[argc++] = last;

        if (argc > 1) {
                errno = 0;
                dirdepth = (size_t) strtol(argv[0], NULL, 10);
                if (errno == ERANGE) {
                        php_error(E_WARNING, "The first parameter in session.save_path is invalid");
                        return FAILURE;
                }
        }

        if (argc > 2) {
                errno = 0;
                filemode = strtol(argv[1], NULL, 8);
                if (errno == ERANGE || filemode < 0 || filemode > 07777) {
                        php_error(E_WARNING, "The second parameter in session.save_path is invalid");
                        return FAILURE;
                }
        }
        save_path = argv[argc - 1];

        data = emalloc(sizeof(*data));
        memset(data, 0, sizeof(*data));

        data->fd = -1;
        data->dirdepth = dirdepth;
        data->filemode = filemode;
        data->basedir_len = strlen(save_path);
        data->basedir = estrndup(save_path, data->basedir_len);

        PS_SET_MOD_DATA(data);

        return SUCCESS;
}
[PHP 5.2.0 session.save_path safe_mode and open_basedir bypass]

Author: Maksymilian Arciemowicz (SecurityReason)
Date:
- - Written: 02.10.2006
- - Public: 08.12.2006
SecurityAlert Id: 43
CVE: CVE-2006-6383
SecurityRisk: High
Affected Software: PHP 5.2.0
Advisory URL: http://securityreason.com/achievement_securityalert/43
Vendor: http://www.php.net

- --- 0.Description ---
PHP is an HTML-embedded scripting language. Much of its syntax is borrowed from
C, Java and Perl with a couple of unique PHP-specific features thrown in. The
goal of the language is to allow web developers to write dynamically generated
pages quickly.

A nice introduction to PHP by Stig Sather Bakken can be found at
http://www.zend.com/zend/art/intro.php on the Zend website. Also, much  of the
PHP Conference Material is freely available.

Session support in PHP consists of a way to preserve certain data across
subsequent accesses. This enables you to build more customized applications and
increase the appeal of your web site.

A visitor accessing your web site is assigned a unique id, the so-called
session id. This is either stored in a cookie on the user side or is propagated
in the URL.

session.save_path defines the argument which is passed to the save handler. If
you choose the default files handler, this is the path where the files are
created. Defaults to /tmp. See also session_save_path().

There is an optional N argument to this directive that determines the number of
directory levels your session files will be spread around in. For example,
setting to '5;/tmp' may end up creating a session file and location like
/tmp/4/b/1/e/3/sess_4b1e384ad74619bd212e236e52a5a174If . In order to use N you
must create all of these directories before use. A small shell script exists in
ext/session to do this, it's called mod_files.sh. Also note that if N is used
and greater than 0 then automatic garbage collection will not be performed, see
a copy of php.ini for further information. Also, if you use N, be sure to
surround session.save_path in "quotes" because the separator (;) is also used
for comments in php.ini.

- --- 1. session.save_path safe mode and open basedir bypass ---
session.save_path can be set in ini_set(), session_save_path() function. In
session.save_path there must be path where you will save yours tmp file. But
syntax for session.save_path can be:

[/PATH]

OR

[N;/PATH]

N - can be a string.

EXAMPLES:

1. session_save_path("/DIR/WHERE/YOU/HAVE/ACCESS")
2. session_save_path("5;/DIR/WHERE/YOU/HAVE/ACCESS")

and

3.
session_save_path("/DIR/WHERE/YOU/DONT/HAVE/ACCESS�;/DIR/WHERE/YOU/HAVE/ACCESS")
CACHE_LIMITER_FUNC(public)
{
        char buf[MAX_STR + 1];
        struct timeval tv;
        time_t now;

        gettimeofday(&tv, NULL);
        now = tv.tv_sec + PS(cache_expire) * 60;
#define EXPIRES "Expires: "
        memcpy(buf, EXPIRES, sizeof(EXPIRES) - 1);
        strcpy_gmt(buf + sizeof(EXPIRES) - 1, &now);
        ADD_HEADER(buf);

        snprintf(buf, sizeof(buf) , "Cache-Control: public, max-age=%ld", PS(cache_expire) * 60); /* SAFE */
        ADD_HEADER(buf);

        last_modified(TSRMLS_C);
}

CACHE_LIMITER_FUNC(private_no_expire)
{
        char buf[MAX_STR + 1];

        snprintf(buf, sizeof(buf), "Cache-Control: private, max-age=%ld, pre-check=%ld", PS(cache_expire) * 60, PS(cache_expire) * 60); /* SAFE */
        ADD_HEADER(buf);

        last_modified(TSRMLS_C);
}

CACHE_LIMITER_FUNC(private)
{
        ADD_HEADER("Expires: Thu, 19 Nov 1981 08:52:00 GMT");
        CACHE_LIMITER(private_no_expire)(TSRMLS_C);
}

CACHE_LIMITER_FUNC(nocache)
{
        ADD_HEADER("Expires: Thu, 19 Nov 1981 08:52:00 GMT");
        /* For HTTP/1.1 conforming clients and the rest (MSIE 5) */
        ADD_HEADER("Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
        /* For HTTP/1.0 conforming clients */
        ADD_HEADER("Pragma: no-cache");
}

static php_session_cache_limiter_t php_session_cache_limiters[] = {
        CACHE_LIMITER_ENTRY(public)
        CACHE_LIMITER_ENTRY(private)
        CACHE_LIMITER_ENTRY(private_no_expire)
        CACHE_LIMITER_ENTRY(nocache)
        {0}
};

static int php_session_cache_limiter(TSRMLS_D)
{
        php_session_cache_limiter_t *lim;

        if (PS(cache_limiter)[0] == '�') return 0;

        if (SG(headers_sent)) {
                char *output_start_filename = php_get_output_start_filename(TSRMLS_C);
                int output_start_lineno = php_get_output_start_lineno(TSRMLS_C);

                if (output_start_filename) {
                        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cache limiter - headers already sent (output started at %s:%d)",
                                output_start_filename, output_start_lineno);
                } else {
                        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cache limiter - headers already sent");
                }
                return -2;
        }

        for (lim = php_session_cache_limiters; lim->name; lim++) {
                if (!strcasecmp(lim->name, PS(cache_limiter))) {
                        lim->func(TSRMLS_C);
                        return 0;
                }
        }

        return -1;
}
static void php_session_send_cookie(TSRMLS_D)
{
        smart_str ncookie = {0};
        char *date_fmt = NULL;
        char *e_session_name, *e_id;

        if (SG(headers_sent)) {
                char *output_start_filename = php_get_output_start_filename(TSRMLS_C);
                int output_start_lineno = php_get_output_start_lineno(TSRMLS_C);

                if (output_start_filename) {
                        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cookie - headers already sent by (output started at %s:%d)",
                                output_start_filename, output_start_lineno);
                } else {
                        php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot send session cookie - headers already sent");
                }
                return;
        }

        /* URL encode session_name and id because they might be user supplied */
        e_session_name = php_url_encode(PS(session_name), strlen(PS(session_name)), NULL);
        e_id = php_url_encode(PS(id), strlen(PS(id)), NULL);

        smart_str_appends(&ncookie, COOKIE_SET_COOKIE);
        smart_str_appends(&ncookie, e_session_name);
        smart_str_appendc(&ncookie, '=');
        smart_str_appends(&ncookie, e_id);

        efree(e_session_name);
        efree(e_id);

        if (PS(cookie_lifetime) > 0) {
                struct timeval tv;
                time_t t;

                gettimeofday(&tv, NULL);
                t = tv.tv_sec + PS(cookie_lifetime);

                if (t > 0) {
                        date_fmt = php_std_date(t TSRMLS_CC);
                        smart_str_appends(&ncookie, COOKIE_EXPIRES);
                        smart_str_appends(&ncookie, date_fmt);
                        efree(date_fmt);
                }
        }

        if (PS(cookie_path)[0]) {
                smart_str_appends(&ncookie, COOKIE_PATH);
                smart_str_appends(&ncookie, PS(cookie_path));
        }

        if (PS(cookie_domain)[0]) {
                smart_str_appends(&ncookie, COOKIE_DOMAIN);
                smart_str_appends(&ncookie, PS(cookie_domain));
        }

        if (PS(cookie_secure)) {
                smart_str_appends(&ncookie, COOKIE_SECURE);
        }

        if (PS(cookie_httponly)) {
                smart_str_appends(&ncookie, COOKIE_HTTPONLY);
        }

        smart_str_0(&ncookie);

        /*      'replace' must be 0 here, else a previous Set-Cookie
                header, probably sent with setcookie() will be replaced! */
        sapi_add_header_ex(ncookie.c, ncookie.len, 0, 0 TSRMLS_CC);
}

PHP PHP Session Session ID

Comments