Difference between revisions for Users / Eo Ny
|
← Previous edit
|
Next edit →
Revision 4 as of
05/05/2026 18:48
▼
6
05/21/2026 16:13
WikiAdmin
5
05/19/2026 11:04
WikiAdmin
4
05/05/2026 18:48
EoNy
3
05/05/2026 18:39
EoNy
(49.6 KiB) +1,595 EoNy |
=== Overview ===
The
Session class is an abstract session management system for WackoWiki that extends ArrayObject to provide secure, configurable session handling. It implements sophisticated security features including session ID regeneration, anti-replay protection, nonce verification, and user agent/IP validation.
Location:
src/class/session.php
Type: Abstract class (must be extended with a SessionStoreInterface implementation)
Inheritance: ##
ArrayObject
=== Table of Contents ===
-
[Core Concepts](#core-concepts)Core Concepts -
[Architecture](#architecture)Architecture -
[Configuration](#configuration)Configuration -
[Usage](#usage)Usage -
[Security Features](#security-features)Security Features -
[API Reference](#api-reference)API Reference -
[Session Lifecycle](#session-lifecycle)Session Lifecycle -
[Flash Data](#flash-data)Flash Data -
[Nonce System](#nonce-system)Nonce System -
[Cookie Management](#cookie-management)Cookie Management -
[Error Handling](#error-handling)Error Handling -
[Implementation Guide](#implementation-guide)
## Implementation Guide
=== Core Concepts
Session State ==== The Session class maintains three primary states:
- Inactive
(`$active** (##$active =false`):false##): Session not yet started or has been closed - Active
(`$active** (##$active =true`):true##): Session is running and can store/retrieve data - Regenerated: Session ID has been replaced (tracked via
`$regenerated`$regeneratedflag)
==== Session Data Storage ==== Session data is stored as an array accessible through
ArrayObject interface:
`php<!--markup:1:end--> <!--markup:2:begin-->%%php<!--markup:2:end--> $session['user_id'] = 123; // Set data echo $session['user_id']; // Get data <!--markup:1:begin-->`
###
====<!--markup:2:end--> Sticky Data <!--markup:2:begin-->====<!--markup:2:end--> Variables prefixed with <!--markup:1:begin-->`sticky_`<!--markup:1:end--> <!--markup:2:begin-->##sticky_##<!--markup:2:end--> are persistent across session resets: - <!--markup:1:begin-->`sticky__created`:<!--markup:1:end--> <!--markup:2:begin-->##sticky__created##:<!--markup:2:end--> Session creation timestamp - <!--markup:1:begin-->`sticky__flash`:<!--markup:1:end--> <!--markup:2:begin-->##sticky__flash##:<!--markup:2:end--> Flash data lifetime tracking - <!--markup:1:begin-->`sticky__log`:<!--markup:1:end--> <!--markup:2:begin-->##sticky__log##:<!--markup:2:end--> Regeneration event log - <!--markup:1:begin-->`sticky__ip`:<!--markup:1:end--> <!--markup:2:begin-->##sticky__ip##:<!--markup:2:end--> IP change tracking <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->====<!--markup:2:end--> Internal Tracking Variables <!--markup:2:begin-->====<!--markup:2:end--> Variables prefixed with <!--markup:1:begin-->`__`<!--markup:1:end--> <!--markup:2:begin-->##__##<!--markup:2:end--> are internal session metadata: - <!--markup:1:begin-->`__started`:<!--markup:1:end--> <!--markup:2:begin-->##__started##:<!--markup:2:end--> Session start time - <!--markup:1:begin-->`__updated`:<!--markup:1:end--> <!--markup:2:begin-->##__updated##:<!--markup:2:end--> Last session update time - <!--markup:1:begin-->`__regenerated`:<!--markup:1:end--> <!--markup:2:begin-->##__regenerated##:<!--markup:2:end--> Last session ID regeneration time - <!--markup:1:begin-->`__user_agent`:<!--markup:1:end--> <!--markup:2:begin-->##__user_agent##:<!--markup:2:end--> Client user agent string - <!--markup:1:begin-->`__user_ip`:<!--markup:1:end--> <!--markup:2:begin-->##__user_ip##:<!--markup:2:end--> Client IP address - <!--markup:1:begin-->`__user_tls`:<!--markup:1:end--> <!--markup:2:begin-->##__user_tls##:<!--markup:2:end--> TLS/SSL status - <!--markup:1:begin-->`__nonces`:<!--markup:1:end--> <!--markup:2:begin-->##__nonces##:<!--markup:2:end--> Active nonce storage - <!--markup:1:begin-->`__expire`:<!--markup:1:end--> <!--markup:2:begin-->##__expire##:<!--markup:2:end--> Session expiration time (for old sessions) <!--markup:1:begin-->--- ##<!--markup:1:end--> <!--markup:2:begin-->---- ===<!--markup:2:end--> Architecture <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->=== ====<!--markup:2:end--> Class Hierarchy <!--markup:1:begin-->```<!--markup:1:end--> <!--markup:2:begin-->====ArrayObject (PHP native)
↓
↓
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> Key Methods Categories <!--markup:2:begin-->====<!--markup:2:end--> **Lifecycle Management:** - <!--markup:1:begin-->`__construct()`:<!--markup:1:end--> <!--markup:2:begin-->##__construct()##:<!--markup:2:end--> Initialize session object - <!--markup:1:begin-->`start()`:<!--markup:1:end--> <!--markup:2:begin-->##start()##:<!--markup:2:end--> Begin a session - <!--markup:1:begin-->`write_close()`:<!--markup:1:end--> <!--markup:2:begin-->##write_close()##:<!--markup:2:end--> Save and close session - <!--markup:1:begin-->`restart()`:<!--markup:1:end--> <!--markup:2:begin-->##restart()##:<!--markup:2:end--> Destroy and restart session - <!--markup:1:begin-->`terminator()`:<!--markup:1:end--> <!--markup:2:begin-->##terminator()##:<!--markup:2:end--> Shutdown handler (garbage collection, flash data cleanup) **Security:** - <!--markup:1:begin-->`regenerate_id()`:<!--markup:1:end--> <!--markup:2:begin-->##regenerate_id()##:<!--markup:2:end--> Replace session ID - <!--markup:1:begin-->`verify_nonce()`:<!--markup:1:end--> <!--markup:2:begin-->##verify_nonce()##:<!--markup:2:end--> Validate nonce tokens - <!--markup:1:begin-->`prevent_replay()`:<!--markup:1:end--> <!--markup:2:begin-->##prevent_replay()##:<!--markup:2:end--> Anti-replay protection - <!--markup:1:begin-->`create_nonce()`:<!--markup:1:end--> <!--markup:2:begin-->##create_nonce()##:<!--markup:2:end--> Generate nonce tokens **Storage (Abstract - Must Implement):** - <!--markup:1:begin-->`store_open()`:<!--markup:1:end--> <!--markup:2:begin-->##store_open()##:<!--markup:2:end--> Open session storage - <!--markup:1:begin-->`store_read()`:<!--markup:1:end--> <!--markup:2:begin-->##store_read()##:<!--markup:2:end--> Read session data - <!--markup:1:begin-->`store_write()`:<!--markup:1:end--> <!--markup:2:begin-->##store_write()##:<!--markup:2:end--> Write session data - <!--markup:1:begin-->`store_close()`:<!--markup:1:end--> <!--markup:2:begin-->##store_close()##:<!--markup:2:end--> Close session storage - <!--markup:1:begin-->`store_gc()`:<!--markup:1:end--> <!--markup:2:begin-->##store_gc()##:<!--markup:2:end--> Garbage collection - <!--markup:1:begin-->`store_validate_id()`:<!--markup:1:end--> <!--markup:2:begin-->##store_validate_id()##:<!--markup:2:end--> Validate session ID format - <!--markup:1:begin-->`store_generate_id()`:<!--markup:1:end--> <!--markup:2:begin-->##store_generate_id()##:<!--markup:2:end--> Generate new session ID **Cookie Management:** - <!--markup:1:begin-->`setcookie()`:<!--markup:1:end--> <!--markup:2:begin-->##setcookie()##:<!--markup:2:end--> Set HTTP cookie with security headers - <!--markup:1:begin-->`get_cookie()`:<!--markup:1:end--> <!--markup:2:begin-->##get_cookie()##:<!--markup:2:end--> Retrieve cookie value - <!--markup:1:begin-->`set_cookie()`:<!--markup:1:end--> <!--markup:2:begin-->##set_cookie()##:<!--markup:2:end--> Set cookie (legacy interface) - <!--markup:1:begin-->`delete_cookie()`:<!--markup:1:end--> <!--markup:2:begin-->##delete_cookie()##:<!--markup:2:end--> Remove cookie - <!--markup:1:begin-->`send_cookie()`:<!--markup:1:end--> <!--markup:2:begin-->##send_cookie()##:<!--markup:2:end--> Internal cookie transmission <!--markup:1:begin-->--- ##<!--markup:1:end--> <!--markup:2:begin-->---- ===<!--markup:2:end--> Configuration <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->=== ====<!--markup:2:end--> Configuration Properties (Public) <!--markup:2:begin-->====<!--markup:2:end--> All configuration properties are prefixed with <!--markup:1:begin-->`cf_`<!--markup:1:end--> <!--markup:2:begin-->##cf_##<!--markup:2:end--> (config) and can be set before calling <!--markup:1:begin-->`start()`: ####<!--markup:1:end--> <!--markup:2:begin-->##start()##: =====<!--markup:2:end--> Session Behavior <!--markup:1:begin-->`php
php<!--markup:2:end--> $session->cf_static = 0; // Disable regenerations (e.g., for CAPTCHA) $session->cf_max_session = 7200; // Max session lifetime (seconds) $session->cf_max_idle = 1440; // Max idle time before destruction (seconds) $session->cf_regen_time = 500; // Seconds between forced ID regenerations $session->cf_regen_probability = 2; // Percentage probability of forced regen (0-100) <!--markup:1:begin-->``` ####<!--markup:1:end--> <!--markup:2:begin-->
Nonce & Replay Protection
`php<!--markup:1:end--> <!--markup:2:begin-->===== %%php<!--markup:2:end--> $session->cf_secret = 'adyaiD9+255JeiskPybgisby'; // Secret for nonce generation $session->cf_nonce_lifetime = 7200; // Nonce expiration (seconds) $session->cf_prevent_replay = 1; // Enable replay attack prevention <!--markup:1:begin-->`
=====<!--markup:2:end--> Garbage Collection <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->=====php $session->cf_gc_probability = 2; // Probability of GC on shutdown (0-100)
$session->cf_gc_maxlifetime = 1440; // Max session file lifetime (seconds)
` ####<!--markup:1:end--> <!--markup:2:begin-->%% =====<!--markup:2:end--> Cookie Settings <!--markup:1:begin-->`php
php<!--markup:2:end-->
$session->cf_cookie_prefix = ''; // Prefix for all cookies
$session->cf_cookie_persistent = false; // Make cookies persistent
$session->cf_cookie_lifetime = 0; // Cookie lifetime (0 = session cookie)
$session->cf_cookie_path = '/'; // Cookie path
$session->cf_cookie_domain = ''; // Cookie domain ('' = current host)
$session->cf_cookie_secure = false; // HTTPS only
$session->cf_cookie_httponly = true; // Disable JavaScript access
$session->cf_cookie_samesite = COOKIE_SAMESITE; // SameSite attribute
<!--markup:1:begin-->```
####<!--markup:1:end-->
<!--markup:2:begin-->
Cache Control
`php<!--markup:1:end--> <!--markup:2:begin-->===== %%php<!--markup:2:end--> $session->cf_cache_limiter = 'none'; // Cache control mode (public|private|nocache|none) $session->cf_cache_expire = 180*60; // Cache TTL (seconds) $session->cf_cache_mtime = 0; // Modify time for Last-Modified header <!--markup:1:begin-->`
=====<!--markup:2:end--> Security Validation <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->=====php $session->cf_referer_check = ''; // Check HTTP Referer header
` ####<!--markup:1:end--> <!--markup:2:begin-->%% =====<!--markup:2:end--> HTTP Context (Set by HTTP class) <!--markup:1:begin-->`php
php<!--markup:2:end--> $session->cf_ip; // Client IP address $session->cf_tls; // TLS/SSL connection indicator <!--markup:1:begin-->``` --- ##<!--markup:1:end--> <!--markup:2:begin-->
=== Usage
Basic Session Setup
`php<!--markup:1:end--> <!--markup:2:begin-->====
%%php<!--markup:2:end-->
// Create a concrete session implementation
class MySession extends Session {
// Implement abstract store_* methods
// See "Implementation Guide" section
}
// Initialize and start session
$session = new MySession();
$session->cf_max_session = 3600; // 1 hour
$session->cf_cookie_path = '/';
$session->start('myapp'); // Session name: 'myapp'
// Store data
$session['user_id'] = 42;
$session['username'] = 'john';
// Retrieve data
echo $session['user_id']; // 42
// Check if session is active
if ($session->active()) {
echo "Session is active";
}
// Explicitly save and close
$session->write_close();
// Shutdown handler automatically called via register_shutdown_function()
<!--markup:1:begin-->
`
###
====<!--markup:2:end--> Session Data Access <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====php // Array-like access (via ArrayObject)
$session['user_id'] = 123;
echo $session['user_id'];
unset($session['user_id']);
isset($session['user_id']);
// Convert to array
$all_data = $session->toArray();
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> Session ID Management <!--markup:1:begin-->`php
php<!--markup:2:end-->
// Get current session ID
$id = $session->id(); // Returns: e.g., "abc123xyz..."
// Get session name
$name = $session->name(); // Returns: 'myapp'
// Get session ID from request
$session->start('myapp', $_REQUEST['sid'] ?? null);
<!--markup:1:begin-->```
###<!--markup:1:end-->
<!--markup:2:begin-->
Session State
`php<!--markup:1:end--> <!--markup:2:begin-->====
%%php<!--markup:2:end-->
// Check if session is active
if ($session->active()) {
// Session is running
}
// Get last state change message
$message = $session->message(); // 'replay', 'ip', 'ua', 'timeout', etc.
// Restart session (destroy old + start new)
$session->restart();
<!--markup:1:begin-->
`
##
---- ===<!--markup:2:end--> Security Features <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->=== ====<!--markup:2:end--> 1. Session ID Regeneration <!--markup:2:begin-->====<!--markup:2:end--> **Purpose:** Prevent session fixation attacks **Automatic Triggers:** - Initial session creation <!--markup:1:begin-->(`regenerated<!--markup:1:end--> <!--markup:2:begin-->(##regenerated<!--markup:2:end--> = <!--markup:1:begin-->2`)<!--markup:1:end--> <!--markup:2:begin-->2##)<!--markup:2:end--> - First request after creation <!--markup:1:begin-->(`regenerated<!--markup:1:end--> <!--markup:2:begin-->(##regenerated<!--markup:2:end--> = <!--markup:1:begin-->1`)<!--markup:1:end--> <!--markup:2:begin-->1##)<!--markup:2:end--> - Periodic forced regeneration (based on <!--markup:1:begin-->`cf_regen_time`<!--markup:1:end--> <!--markup:2:begin-->##cf_regen_time##<!--markup:2:end--> and <!--markup:1:begin-->`cf_regen_probability`)<!--markup:1:end--> <!--markup:2:begin-->##cf_regen_probability##)<!--markup:2:end--> - Session validation failures **Manual Trigger:** <!--markup:1:begin-->```php<!--markup:1:end-->** <!--markup:2:begin-->php $session->regenerate_id($delete_old = false, $message = 'custom_reason');
`<!--markup:1:end--> <!--markup:2:begin-->%%<!--markup:2:end--> **Parameters:** - <!--markup:1:begin-->`$delete_old`:<!--markup:1:end--> <!--markup:2:begin-->##$delete_old##:<!--markup:2:end--> - <!--markup:1:begin-->`false`<!--markup:1:end--> <!--markup:2:begin-->##false##<!--markup:2:end--> (0): Keep old session active for ~5 seconds (for pending AJAX requests) - <!--markup:1:begin-->`true`<!--markup:1:end--> <!--markup:2:begin-->##true##<!--markup:2:end--> (1): Keep old session for time specified (unused in current code) - <!--markup:1:begin-->`2`:<!--markup:1:end--> <!--markup:2:begin-->##2##:<!--markup:2:end--> Immediately destroy old session **Implementation Details:** - New session ID is generated via <!--markup:1:begin-->`store_generate_id()`<!--markup:1:end--> <!--markup:2:begin-->##store_generate_id()##<!--markup:2:end--> - Old session data is copied to new ID - Old session marked with <!--markup:1:begin-->`__expire`<!--markup:1:end--> <!--markup:2:begin-->##__expire##<!--markup:2:end--> timestamp - Cookie immediately updated with new ID - Single regeneration per request (checked via <!--markup:1:begin-->`$this->regenerated`<!--markup:1:end--> <!--markup:2:begin-->##$this->regenerated##<!--markup:2:end--> flag) - Logged in <!--markup:1:begin-->`sticky__log`<!--markup:1:end--> <!--markup:2:begin-->##sticky__log##<!--markup:2:end--> for debugging (max 15 entries) <!--markup:1:begin-->`php
php<!--markup:2:end-->
// Example: Force regeneration on login
$session->start('myapp');
if ($user_authenticated) {
$session->regenerate_id(false, 'login');
$session['user_id'] = $user->id;
}
<!--markup:1:begin-->```
###<!--markup:1:end-->
<!--markup:2:begin-->
2. User Agent Validation ====
Purpose: Detect browser/device changes that might indicate hijacking
Behavior:
- Stores user agent on first request
- Compares on subsequent requests using
`similar_text()`similar_text() - Destroys session if similarity < 95%
- Useful against bot attacks or stolen sessions
Configuration:
`php<!--markup:1:end-->** <!--markup:2:begin-->%%php<!--markup:2:end--> // Automatic on each request (if enabled in code logic) // Triggers session destruction if UA changes significantly <!--markup:1:begin-->`
###
====<!--markup:2:end--> 3. IP Address Validation <!--markup:2:begin-->====<!--markup:2:end--> **Purpose:** Detect IP spoofing or hijacking **Behavior:** - Stores IP on first request - Compares on subsequent requests - Soft failure on mismatch: <!--markup:1:begin-->`destroy<!--markup:1:end--> <!--markup:2:begin-->##destroy<!--markup:2:end--> = <!--markup:1:begin-->1`<!--markup:1:end--> <!--markup:2:begin-->1##<!--markup:2:end--> (keeps regenerating) - Tracks IP changes in <!--markup:1:begin-->`sticky__ip`<!--markup:1:end--> <!--markup:2:begin-->##sticky__ip##<!--markup:2:end--> **Configuration:** <!--markup:1:begin-->```php<!--markup:1:end-->** <!--markup:2:begin-->php $session->cf_ip = $_SERVER['REMOTE_ADDR']; // Set by HTTP class
// Validation happens automatically during start()
`<!--markup:1:end--> <!--markup:2:begin-->%%<!--markup:2:end--> **IP Change Tracking:** <!--markup:1:begin-->`php
php<!--markup:2:end--> // Access IP change history $ip_history = $session->sticky__ip; // Array of [ip => change_count] <!--markup:1:begin-->``` ###<!--markup:1:end--> <!--markup:2:begin-->
4. TLS/SSL Validation ====
Purpose: Prevent protocol downgrade attacks
Behavior:
- Checks if connection transitioned from HTTPS to HTTP
- Destroys session on mismatch
Configuration:
`php<!--markup:1:end-->** <!--markup:2:begin-->%%php<!--markup:2:end--> $session->cf_tls = !empty($_SERVER['HTTPS']); // Set by HTTP class // Validation happens automatically during start() <!--markup:1:begin-->`
###
====<!--markup:2:end--> 5. Anti-Replay Protection <!--markup:2:begin-->====<!--markup:2:end--> **Purpose:** Prevent CSRF and replay attacks **Mechanism:** - Generates unique "NoReplay" nonce on each request - Cookie-based nonce verification - Detects rapid-fire requests (AJAX attacks) **Configuration:** <!--markup:1:begin-->```php<!--markup:1:end-->** <!--markup:2:begin-->php $session->cf_prevent_replay = 1; // Enable (default)
$session->cf_prevent_replay = 0; // Disable if needed
`<!--markup:1:end--> <!--markup:2:begin-->%%<!--markup:2:end--> **How It Works:** <!--markup:1:begin-->`
<!--markup:2:end--> Request 1: Generate nonce, send in cookie Request 2: Client sends nonce back, verify & generate new one Request 3: If old nonce used again → reject (replay detected) <!--markup:1:begin-->``` ###<!--markup:1:end--> <!--markup:2:begin-->
6. Referer Validation (Optional) ====
Purpose: Prevent CSRF via header checking
Configuration:
`php<!--markup:1:end-->** <!--markup:2:begin-->%%php<!--markup:2:end--> $session->cf_referer_check = 'example.com'; // Session rejected if HTTP_REFERER doesn't contain this string <!--markup:1:begin-->`
##
---- ===<!--markup:2:end--> API Reference <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->=== ====<!--markup:2:end--> Public Methods <!--markup:1:begin-->####<!--markup:1:end--> <!--markup:2:begin-->==== =====<!--markup:2:end--> Lifecycle Management <!--markup:1:begin-->##### `start($name<!--markup:1:end--> <!--markup:2:begin-->===== ====== ##start($name<!--markup:2:end--> = null, $id = null): <!--markup:1:begin-->bool`<!--markup:1:end--> <!--markup:2:begin-->bool## ======<!--markup:2:end--> Start or resume a session. **Parameters:** - <!--markup:1:begin-->`$name`<!--markup:1:end--> <!--markup:2:begin-->##$name##<!--markup:2:end--> (string|null): Session name (cookie name base). Alphanumeric + underscore/dash. Defaults to 'sesid' - <!--markup:1:begin-->`$id`<!--markup:1:end--> <!--markup:2:begin-->##$id##<!--markup:2:end--> (string|null): Existing session ID to resume. If null, attempts to read from cookie **Returns:** <!--markup:1:begin-->`true`<!--markup:1:end-->** <!--markup:2:begin-->##true##<!--markup:2:end--> if session started successfully, <!--markup:1:begin-->`false`<!--markup:1:end--> <!--markup:2:begin-->##false##<!--markup:2:end--> on error **Side Effects:** - Sets headers (cookies, cache control) - Populates session data from storage - Performs security validations - May trigger session ID regeneration **Example:** <!--markup:1:begin-->```php<!--markup:1:end-->** <!--markup:2:begin-->php if ($session->start('webapp', $_COOKIE['sess_id'] ?? null)) {
// Session ready
// Session failed
`<!--markup:1:end--> <!--markup:2:begin-->%%<!--markup:2:end--> **Validation Steps:** 1. Reject if headers already sent 2. Validate session name format 3. Retrieve ID from parameter or cookie 4. Check Referer header (if configured) 5. Validate ID format via <!--markup:1:begin-->`store_validate_id()`<!--markup:1:end--> <!--markup:2:begin-->##store_validate_id()##<!--markup:2:end--> 6. Read session data from storage 7. Verify nonces and timestamps 8. Check user agent, IP, TLS 9. Regenerate if needed <!--markup:1:begin-->--- ##### `write_close(): void`<!--markup:1:end--> <!--markup:2:begin-->---- ====== ##write_close(): void## ======<!--markup:2:end--> Save session data and close session. **Side Effects:** - Calls <!--markup:1:begin-->`write_session()`<!--markup:1:end--> <!--markup:2:begin-->##write_session()##<!--markup:2:end--> to serialize and store data - Calls <!--markup:1:begin-->`store_close()`<!--markup:1:end--> <!--markup:2:begin-->##store_close()##<!--markup:2:end--> to close storage handler - Sets <!--markup:1:begin-->`$active<!--markup:1:end--> <!--markup:2:begin-->##$active<!--markup:2:end--> = <!--markup:1:begin-->false`<!--markup:1:end--> <!--markup:2:begin-->false##<!--markup:2:end--> **Example:** <!--markup:1:begin-->`php
php<!--markup:2:end--> $session['key'] = 'value'; $session->write_close(); // Ensure data is saved <!--markup:1:begin-->``` --- ##### `restart(): bool`<!--markup:1:end--> <!--markup:2:begin-->
restart(): bool
Destroy current session and create new one.Equivalent to:
Returns:
true on success, false on error
Use Cases:
- User logout and new login
- Security reset
- Complete session refresh
Example:
`php<!--markup:1:end-->** <!--markup:2:begin-->%%php<!--markup:2:end--> $session->restart(); // New session created, old data cleared, sticky_ vars preserved <!--markup:1:begin-->`
---- =====<!--markup:2:end--> Session Access <!--markup:1:begin-->##### `id(): mixed`<!--markup:1:end--> <!--markup:2:begin-->===== ====== ##id(): mixed## ======<!--markup:2:end--> Get current session ID. **Returns:** Session ID string or null if not started <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->php $sid = $session->id(); // "abc123xyz..."
` --- ##### `name(): string`<!--markup:1:end--> <!--markup:2:begin-->%% ---- ====== ##name(): string## ======<!--markup:2:end--> Get session name (cookie prefix). **Returns:** Session name <!--markup:1:begin-->`php
php<!--markup:2:end--> $name = $session->name(); // "myapp" <!--markup:1:begin-->``` --- ##### `active(): bool`<!--markup:1:end--> <!--markup:2:begin-->
active(): bool
Check if session is currently active.Returns:
true if session is started and active, false otherwise
`php<!--markup:1:end-->
<!--markup:2:begin-->%%php<!--markup:2:end-->
if ($session->active()) {
$session['key'] = 'value';
}
<!--markup:1:begin-->
`
# `message(): string|null`---- ====== ##message(): string|null## ======<!--markup:2:end--> Get reason for last session state change. **Returns:** Message string or null **Possible Values:** - <!--markup:1:begin-->`'replay'`:<!--markup:1:end--> <!--markup:2:begin-->##'replay'##:<!--markup:2:end--> Replay attack detected - <!--markup:1:begin-->`'obsolete'`:<!--markup:1:end--> <!--markup:2:begin-->##'obsolete'##:<!--markup:2:end--> Session marked for expiration - <!--markup:1:begin-->`'reg_expire'`:<!--markup:1:end--> <!--markup:2:begin-->##'reg_expire'##:<!--markup:2:end--> Regeneration expiration reached - <!--markup:1:begin-->`'max_session'`:<!--markup:1:end--> <!--markup:2:begin-->##'max_session'##:<!--markup:2:end--> Max session lifetime exceeded - <!--markup:1:begin-->`'max_idle'`:<!--markup:1:end--> <!--markup:2:begin-->##'max_idle'##:<!--markup:2:end--> Idle timeout exceeded - <!--markup:1:begin-->`'ua'`:<!--markup:1:end--> <!--markup:2:begin-->##'ua'##:<!--markup:2:end--> User agent mismatch (>5% difference) - <!--markup:1:begin-->`'tls'`:<!--markup:1:end--> <!--markup:2:begin-->##'tls'##:<!--markup:2:end--> TLS status changed - <!--markup:1:begin-->`'ip'`:<!--markup:1:end--> <!--markup:2:begin-->##'ip'##:<!--markup:2:end--> IP address mismatch - <!--markup:1:begin-->`'restart'`:<!--markup:1:end--> <!--markup:2:begin-->##'restart'##:<!--markup:2:end--> Session manually restarted - <!--markup:1:begin-->`null`:<!--markup:1:end--> <!--markup:2:begin-->##null##:<!--markup:2:end--> No state change **Example:** <!--markup:1:begin-->```php<!--markup:1:end-->** <!--markup:2:begin-->php $session->start('app');
if ($message = $session->message()) {
error_log("Session issue: $message");
` --- ##### `toArray(): array`<!--markup:1:end--> <!--markup:2:begin-->%% ---- ====== ##toArray(): array## ======<!--markup:2:end--> Convert session data to array. **Returns:** Associative array of session data **Note:** This is a direct call to <!--markup:1:begin-->`ArrayObject::getArrayCopy()``php
ArrayObject::getArrayCopy()
php<!--markup:2:end-->
$data = $session->toArray();
foreach ($data as $key => $value) {
echo "$key => $value\n";
}
<!--markup:1:begin-->```
---
####<!--markup:1:end-->
<!--markup:2:begin-->
Nonce System
# `create_nonce($action,##create_nonce($action, $expires = null):
Parameters:
-
`$action`$action(string): Action identifier (e.g., 'form_submit', 'delete_action') -
`$expires`$expires(int|null): Expiration time in seconds. Defaults to`cf_nonce_lifetime`cf_nonce_lifetime
Returns: Nonce token string (11 characters)
Example:
`php<!--markup:1:end-->**
<!--markup:2:begin-->%%php<!--markup:2:end-->
$nonce = $session->create_nonce('form_submit', 3600);
// Use in HTML: <input type="hidden" name="nonce" value="<?= $nonce ?>">
<!--markup:1:begin-->
`
<!--markup:2:end-->
**Storage:**
- Stored in <!--markup:1:begin-->`$session->__nonces[]`<!--markup:1:end--> <!--markup:2:begin-->##$session->__nonces[]##<!--markup:2:end-->
- Key: <!--markup:1:begin-->`{action}.{base64_encoded_hash}`<!--markup:1:end--> <!--markup:2:begin-->##{action}.{base64_encoded_hash}##<!--markup:2:end-->
- Value: Expiration timestamp
<!--markup:1:begin-->---
##### `verify_nonce($action,<!--markup:1:end-->
<!--markup:2:begin-->----
====== ##verify_nonce($action,<!--markup:2:end--> $code, $protect = <!--markup:1:begin-->0)`<!--markup:1:end--> <!--markup:2:begin-->0)## ======<!--markup:2:end-->
Verify a nonce token.
**Parameters:**
- <!--markup:1:begin-->`$action`<!--markup:1:end--> <!--markup:2:begin-->##$action##<!--markup:2:end--> (string): Action identifier that was used in <!--markup:1:begin-->`create_nonce()`<!--markup:1:end--> <!--markup:2:begin-->##create_nonce()##<!--markup:2:end-->
- <!--markup:1:begin-->`$code`<!--markup:1:end--> <!--markup:2:begin-->##$code##<!--markup:2:end--> (string): Nonce token from user
- <!--markup:1:begin-->`$protect`<!--markup:1:end--> <!--markup:2:begin-->##$protect##<!--markup:2:end--> (int): Protection level
- <!--markup:1:begin-->`0`:<!--markup:1:end--> <!--markup:2:begin-->##0##:<!--markup:2:end--> Single-use nonce (consumed on first verification)
- <!--markup:1:begin-->`1+`:<!--markup:1:end--> <!--markup:2:begin-->##1+##:<!--markup:2:end--> Protected nonce (can verify multiple times, prevents fast replays)
**Returns:**
- <!--markup:1:begin-->`true`<!--markup:1:end--> <!--markup:2:begin-->##true##<!--markup:2:end--> (1): Nonce verified and valid
- <!--markup:1:begin-->`false`<!--markup:1:end--> <!--markup:2:begin-->##false##<!--markup:2:end--> (0): Nonce invalid or expired
- <!--markup:1:begin-->`-1`:<!--markup:1:end--> <!--markup:2:begin-->##-1##:<!--markup:2:end--> Protected nonce used twice in quick succession (possible AJAX attack)
**Example:**
<!--markup:1:begin-->```php<!--markup:1:end-->**
<!--markup:2:begin-->
php
if ($nonce = $session->verify_nonce('form_submit', $_POST['nonce'])) {if ($nonce === -1) {
} else {
}
// Possible replay, but might be legitimate AJAX
$session->cf_prevent_replay = 0; // Disable for this request
$session->cf_prevent_replay = 0; // Disable for this request
// Safe to process
process_form();
process_form();
`<!--markup:1:end--> <!--markup:2:begin-->%%<!--markup:2:end--> **Cleanup:** - Expired nonces automatically removed - Verified single-use nonces removed from storage <!--markup:1:begin-->--- ####<!--markup:1:end--> <!--markup:2:begin-->---- =====<!--markup:2:end--> Cookie Management <!--markup:1:begin-->##### `setcookie($name,<!--markup:1:end--> <!--markup:2:begin-->===== ====== ##setcookie($name,<!--markup:2:end--> $value = null, $expires = 0, $path = null, $domain = null, $secure = null, $httponly = null, $samesite = null): <!--markup:1:begin-->bool`<!--markup:1:end--> <!--markup:2:begin-->bool## ======<!--markup:2:end--> Set a cookie with security headers. **Parameters:** - <!--markup:1:begin-->`$name`:<!--markup:1:end--> <!--markup:2:begin-->##$name##:<!--markup:2:end--> Cookie name (automatically URL-encoded) - <!--markup:1:begin-->`$value`:<!--markup:1:end--> <!--markup:2:begin-->##$value##:<!--markup:2:end--> Cookie value (automatically URL-encoded, null to delete) - <!--markup:1:begin-->`$expires`:<!--markup:1:end--> <!--markup:2:begin-->##$expires##:<!--markup:2:end--> Expiration timestamp (0 = session cookie) - <!--markup:1:begin-->`$path`:<!--markup:1:end--> <!--markup:2:begin-->##$path##:<!--markup:2:end--> Cookie path (default: <!--markup:1:begin-->`cf_cookie_path`)<!--markup:1:end--> <!--markup:2:begin-->##cf_cookie_path##)<!--markup:2:end--> - <!--markup:1:begin-->`$domain`:<!--markup:1:end--> <!--markup:2:begin-->##$domain##:<!--markup:2:end--> Cookie domain (default: <!--markup:1:begin-->`cf_cookie_domain`)<!--markup:1:end--> <!--markup:2:begin-->##cf_cookie_domain##)<!--markup:2:end--> - <!--markup:1:begin-->`$secure`:<!--markup:1:end--> <!--markup:2:begin-->##$secure##:<!--markup:2:end--> HTTPS only (default: <!--markup:1:begin-->`cf_cookie_secure`)<!--markup:1:end--> <!--markup:2:begin-->##cf_cookie_secure##)<!--markup:2:end--> - <!--markup:1:begin-->`$httponly`:<!--markup:1:end--> <!--markup:2:begin-->##$httponly##:<!--markup:2:end--> Disable JS access (default: <!--markup:1:begin-->`cf_cookie_httponly`)<!--markup:1:end--> <!--markup:2:begin-->##cf_cookie_httponly##)<!--markup:2:end--> - <!--markup:1:begin-->`$samesite`:<!--markup:1:end--> <!--markup:2:begin-->##$samesite##:<!--markup:2:end--> SameSite attribute (default: <!--markup:1:begin-->`cf_cookie_samesite`)<!--markup:1:end--> <!--markup:2:begin-->##cf_cookie_samesite##)<!--markup:2:end--> **Returns:** <!--markup:1:begin-->`true`<!--markup:1:end-->** <!--markup:2:begin-->##true##<!--markup:2:end--> on success, <!--markup:1:begin-->`false`<!--markup:1:end--> <!--markup:2:begin-->##false##<!--markup:2:end--> if headers already sent **Features:** - RFC 2616 2.2 token encoding for cookie name - RFC 6265 4.1.1 cookie-octet encoding for value - Removes duplicate cookie headers automatically - Adds all security attributes (secure, httponly, samesite) - Does NOT replace existing cookies (allows multiple Set-Cookie headers) **Example:** <!--markup:1:begin-->`php
php<!--markup:2:end-->
// Session cookie
$session->setcookie('user_pref', 'dark_mode');
// Persistent cookie (30 days)
$session->setcookie('remember_me', 'token123', time() + 30*86400);
// Delete cookie
$session->setcookie('old_cookie', null);
// Secure cookie with SameSite
$session->setcookie('token', 'abc123', time() + 3600,
path: '/', secure: true, httponly: true, samesite: 'Strict');
<!--markup:1:begin-->```
---
##### `get_cookie($name)`<!--markup:1:end-->
<!--markup:2:begin-->
get_cookie($name)
Retrieve cookie value.Parameters:
-
`$name`:$name: Cookie name (prefix automatically added)
Returns: Cookie value or null if not set
`php<!--markup:1:end-->
<!--markup:2:begin-->%%php<!--markup:2:end-->
$value = $session->get_cookie('user_pref'); // Reads $_COOKIE['user_pref']
<!--markup:1:begin-->
`
# `set_cookie($name,---- ====== ##set_cookie($name,<!--markup:2:end--> $value, $persistent = false): <!--markup:1:begin-->void`<!--markup:1:end--> <!--markup:2:begin-->void## ======<!--markup:2:end--> Legacy cookie setter (alternative to <!--markup:1:begin-->`setcookie()`).<!--markup:1:end--> <!--markup:2:begin-->##setcookie()##).<!--markup:2:end--> **Parameters:** - <!--markup:1:begin-->`$name`:<!--markup:1:end--> <!--markup:2:begin-->##$name##:<!--markup:2:end--> Cookie name (prefix added) - <!--markup:1:begin-->`$value`:<!--markup:1:end--> <!--markup:2:begin-->##$value##:<!--markup:2:end--> Cookie value - <!--markup:1:begin-->`$persistent`:<!--markup:1:end--> <!--markup:2:begin-->##$persistent##:<!--markup:2:end--> - <!--markup:1:begin-->`false`:<!--markup:1:end--> <!--markup:2:begin-->##false##:<!--markup:2:end--> Session cookie (deleted on browser close) - Number: Days to persist - <!--markup:1:begin-->`0`:<!--markup:1:end--> <!--markup:2:begin-->##0##:<!--markup:2:end--> Use <!--markup:1:begin-->`cf_cookie_persistent`<!--markup:1:end--> <!--markup:2:begin-->##cf_cookie_persistent##<!--markup:2:end--> config **Example:** <!--markup:1:begin-->```php<!--markup:1:end-->** <!--markup:2:begin-->php $session->set_cookie('theme', 'dark'); // Session cookie
$session->set_cookie('lang', 'en', 365); // 1 year
` --- ##### `delete_cookie($name): void`<!--markup:1:end--> <!--markup:2:begin-->%% ---- ====== ##delete_cookie($name): void## ======<!--markup:2:end--> Delete a cookie. **Parameters:** - <!--markup:1:begin-->`$name`:<!--markup:1:end--> <!--markup:2:begin-->##$name##:<!--markup:2:end--> Cookie name (prefix added) **Implementation:** Sets empty value with immediate expiration <!--markup:1:begin-->`php
php<!--markup:2:end-->
$session->delete_cookie('old_preference');
<!--markup:1:begin-->```
---
##### `unsetcookie($name): void`<!--markup:1:end-->
<!--markup:2:begin-->
unsetcookie($name): void
Alias for setcookie($name) with no value (convenience method).
`php<!--markup:1:end-->
<!--markup:2:begin-->%%php<!--markup:2:end-->
$session->unsetcookie('cookie_name');
<!--markup:1:begin-->
`
###
---- ====<!--markup:2:end--> Protected Methods (For Store Implementation) <!--markup:1:begin-->#### `regenerate_id($delete_old<!--markup:1:end--> <!--markup:2:begin-->==== ===== ##regenerate_id($delete_old<!--markup:2:end--> = false, $message = ''): <!--markup:1:begin-->bool`<!--markup:1:end--> <!--markup:2:begin-->bool## =====<!--markup:2:end--> Internal method to regenerate session ID (called automatically). **Protected** - Usually called automatically, but can be overridden/called by subclasses <!--markup:1:begin-->--- #### `store_generate_id(): string`<!--markup:1:end--> <!--markup:2:begin-->---- ===== ##store_generate_id(): string## =====<!--markup:2:end--> Generate a new session ID. **Default Implementation:** Returns 21-character random alphanumeric string via <!--markup:1:begin-->`Ut::random_token(21)`<!--markup:1:end--> <!--markup:2:begin-->##Ut::random_token(21)##<!--markup:2:end--> **Override in subclass to customize:** <!--markup:1:begin-->```php<!--markup:1:end-->** <!--markup:2:begin-->php protected function store_generate_id(): string {
return hash('sha256', random_bytes(32)); // Your format
`
---
#### `store_validate_id($id): bool`<!--markup:1:end-->
<!--markup:2:begin-->%%
----
===== ##store_validate_id($id): bool## =====<!--markup:2:end-->
Validate session ID format.
**Default Implementation:** Regex check: <!--markup:1:begin-->`/^[a-zA-Z\d]{21}$/`<!--markup:1:end--> <!--markup:2:begin-->##/^[a-zA-Z\d]{21}$/##<!--markup:2:end-->
**Override in subclass to match your format:**
<!--markup:1:begin-->
`php
php<!--markup:2:end-->
protected function store_validate_id($id): bool {
return preg_match('/^[a-f0-9]{64}$/', $id); // SHA256 format
}
<!--markup:1:begin-->```
---
#### `store_open($name): void`<!--markup:1:end-->
<!--markup:2:begin-->
store_open($name): void
Open session storage (called before first read/write).Subclass must implement - Initialize storage handler
Example:
`php<!--markup:1:end-->**
<!--markup:2:begin-->%%php<!--markup:2:end-->
protected function store_open($name): void {
$this->db = new PDO('sqlite::memory:');
}
<!--markup:1:begin-->
`
`store_read($id,---- ===== ##store_read($id,<!--markup:2:end--> $lock = false): <!--markup:1:begin-->string|false`<!--markup:1:end--> <!--markup:2:begin-->string|false## =====<!--markup:2:end--> Read session data from storage. **Subclass must implement** **Parameters:** - <!--markup:1:begin-->`$id`:<!--markup:1:end--> <!--markup:2:begin-->##$id##:<!--markup:2:end--> Session ID to read - <!--markup:1:begin-->`$lock`:<!--markup:1:end--> <!--markup:2:begin-->##$lock##:<!--markup:2:end--> If true, lock the session file for writing (create new) **Returns:** - Serialized session data (string) if found and locked - Empty string <!--markup:1:begin-->(`''`)<!--markup:1:end--> <!--markup:2:begin-->(##''##)<!--markup:2:end--> if new session should be created - <!--markup:1:begin-->`false`<!--markup:1:end--> <!--markup:2:begin-->##false##<!--markup:2:end--> if session doesn't exist or read error **Example:** <!--markup:1:begin-->```php<!--markup:1:end-->** <!--markup:2:begin-->php protected function store_read($id, $lock = false): string|false {
$data = file_get_contents("/tmp/sess_$id");
return $data ?: false;
return $data ?: false;
` --- #### `store_write($id,<!--markup:1:end--> <!--markup:2:begin-->%% ---- ===== ##store_write($id,<!--markup:2:end--> $data): <!--markup:1:begin-->void`<!--markup:1:end--> <!--markup:2:begin-->void## =====<!--markup:2:end--> Write session data to storage. **Subclass must implement** **Parameters:** - <!--markup:1:begin-->`$id`:<!--markup:1:end--> <!--markup:2:begin-->##$id##:<!--markup:2:end--> Session ID - <!--markup:1:begin-->`$data`:<!--markup:1:end--> <!--markup:2:begin-->##$data##:<!--markup:2:end--> Serialized session data (already processed by <!--markup:1:begin-->`Ut::serialize()`)<!--markup:1:end--> <!--markup:2:begin-->##Ut::serialize()##)<!--markup:2:end--> **Example:** <!--markup:1:begin-->`php
php<!--markup:2:end-->
protected function store_write($id, $data): void {
file_put_contents("/tmp/sess_$id", $data);
}
<!--markup:1:begin-->```
---
#### `store_close(): void`<!--markup:1:end-->
<!--markup:2:begin-->
store_close(): void
Close session storage.Subclass must implement - Release resources
Example:
`php<!--markup:1:end-->**
<!--markup:2:begin-->%%php<!--markup:2:end-->
protected function store_close(): void {
// Close database, file, etc.
}
<!--markup:1:begin-->
`
`store_gc(): void`---- ===== ##store_gc(): void## =====<!--markup:2:end--> Perform garbage collection on old sessions. **Subclass must implement** - Delete expired sessions **Called During:** - Shutdown handler (probabilistic, based on <!--markup:1:begin-->`cf_gc_probability`)<!--markup:1:end--> <!--markup:2:begin-->##cf_gc_probability##)<!--markup:2:end--> **Should Delete:** - Sessions older than <!--markup:1:begin-->`cf_gc_maxlifetime`<!--markup:1:end--> <!--markup:2:begin-->##cf_gc_maxlifetime##<!--markup:2:end--> seconds **Example:** <!--markup:1:begin-->```php<!--markup:1:end-->** <!--markup:2:begin-->php protected function store_gc(): void {
$max_age = time() - $this->cf_gc_maxlifetime;
// Delete files/records older than $max_age
// Delete files/records older than $max_age
`
---
###<!--markup:1:end-->
<!--markup:2:begin-->%%
----
====<!--markup:2:end--> Private Methods (Internal Use)
<!--markup:1:begin-->##### `populate(): void`<!--markup:1:end--> <!--markup:2:begin-->====
====== ##populate(): void## ======<!--markup:2:end-->
Initialize session tracking variables on first request.
**Called by:** <!--markup:1:begin-->`start()`, `restart()`<!--markup:1:end-->** <!--markup:2:begin-->##start()##, ##restart()##<!--markup:2:end-->
**Initializes:**
- <!--markup:1:begin-->`__started`:<!--markup:1:end--> <!--markup:2:begin-->##__started##:<!--markup:2:end--> Current timestamp
- <!--markup:1:begin-->`__regenerated`:<!--markup:1:end--> <!--markup:2:begin-->##__regenerated##:<!--markup:2:end--> Current timestamp
- <!--markup:1:begin-->`__user_agent`:<!--markup:1:end--> <!--markup:2:begin-->##__user_agent##:<!--markup:2:end--> Browser user agent
- <!--markup:1:begin-->`__user_ip`:<!--markup:1:end--> <!--markup:2:begin-->##__user_ip##:<!--markup:2:end--> Client IP (if configured)
- <!--markup:1:begin-->`__user_tls`:<!--markup:1:end--> <!--markup:2:begin-->##__user_tls##:<!--markup:2:end--> TLS status (if configured)
- <!--markup:1:begin-->`sticky__created`:<!--markup:1:end--> <!--markup:2:begin-->##sticky__created##:<!--markup:2:end--> Creation time (if not exists)
<!--markup:1:begin-->---
##### `write_session(): void`<!--markup:1:end-->
<!--markup:2:begin-->----
====== ##write_session(): void## ======<!--markup:2:end-->
Serialize and write session data to storage.
**Called by:** <!--markup:1:begin-->`regenerate_id()`, `write_close()`, `terminator()`<!--markup:1:end-->** <!--markup:2:begin-->##regenerate_id()##, ##write_close()##, ##terminator()##<!--markup:2:end-->
**Updates:**
- <!--markup:1:begin-->`__updated`:<!--markup:1:end--> <!--markup:2:begin-->##__updated##:<!--markup:2:end--> Current timestamp
- Calls <!--markup:1:begin-->`store_write()`<!--markup:1:end--> <!--markup:2:begin-->##store_write()##<!--markup:2:end--> with serialized data
<!--markup:1:begin-->---
##### `clean_vars(): void`<!--markup:1:end-->
<!--markup:2:begin-->----
====== ##clean_vars(): void## ======<!--markup:2:end-->
Remove non-sticky session variables.
**Called by:** <!--markup:1:begin-->`restart()`,<!--markup:1:end-->** <!--markup:2:begin-->##restart()##,<!--markup:2:end--> session validation failure
**Preserves:** Variables starting with <!--markup:1:begin-->`sticky_`
---
##### `prevent_replay(): void`<!--markup:1:end--> <!--markup:2:begin-->##sticky_##
----
====== ##prevent_replay(): void## ======<!--markup:2:end-->
Generate and send anti-replay nonce.
**Called by:** <!--markup:1:begin-->`populate()`<!--markup:1:end-->** <!--markup:2:begin-->##populate()##<!--markup:2:end-->
**Action:**
- Creates 'NoReplay' nonce
- Sends in cookie: <!--markup:1:begin-->`{cf_cookie_prefix}NoReplay`
---
##### `cache_limiter(): void`<!--markup:1:end--> <!--markup:2:begin-->##{cf_cookie_prefix}NoReplay##
----
====== ##cache_limiter(): void## ======<!--markup:2:end-->
Set HTTP cache control headers based on configuration.
**Called by:** <!--markup:1:begin-->`start()`<!--markup:1:end-->** <!--markup:2:begin-->##start()##<!--markup:2:end--> after session data loaded
**Modes:**
- <!--markup:1:begin-->`'public'`:<!--markup:1:end--> <!--markup:2:begin-->##'public'##:<!--markup:2:end--> Cacheable, <!--markup:1:begin-->`Cache-Control:<!--markup:1:end--> <!--markup:2:begin-->##Cache-Control:<!--markup:2:end--> public, <!--markup:1:begin-->max-age=...`<!--markup:1:end--> <!--markup:2:begin-->max-age=...##<!--markup:2:end-->
- <!--markup:1:begin-->`'private'`:<!--markup:1:end--> <!--markup:2:begin-->##'private'##:<!--markup:2:end--> Private, <!--markup:1:begin-->`Cache-Control:<!--markup:1:end--> <!--markup:2:begin-->##Cache-Control:<!--markup:2:end--> private, <!--markup:1:begin-->max-age=...`<!--markup:1:end--> <!--markup:2:begin-->max-age=...##<!--markup:2:end-->
- <!--markup:1:begin-->`'private_no_expire'`:<!--markup:1:end--> <!--markup:2:begin-->##'private_no_expire'##:<!--markup:2:end--> Private no TTL
- <!--markup:1:begin-->`'nocache'`:<!--markup:1:end--> <!--markup:2:begin-->##'nocache'##:<!--markup:2:end--> No storage, <!--markup:1:begin-->`Cache-Control: no-store`<!--markup:1:end--> <!--markup:2:begin-->##Cache-Control: no-store##<!--markup:2:end-->
- <!--markup:1:begin-->`'none'`:<!--markup:1:end--> <!--markup:2:begin-->##'none'##:<!--markup:2:end--> No headers (default)
<!--markup:1:begin-->---
##### `set_new_id(): void`<!--markup:1:end-->
<!--markup:2:begin-->----
====== ##set_new_id(): void## ======<!--markup:2:end-->
Generate and assign new session ID, send in cookie.
**Called by:** <!--markup:1:begin-->`regenerate_id()`, `start()`<!--markup:1:end-->** <!--markup:2:begin-->##regenerate_id()##, ##start()##<!--markup:2:end--> (for new sessions)
<!--markup:1:begin-->---
##### `remove_cookie($cookie): void`<!--markup:1:end-->
<!--markup:2:begin-->----
====== ##remove_cookie($cookie): void## ======<!--markup:2:end-->
Remove existing Set-Cookie header to avoid duplicates.
**Called by:** <!--markup:1:begin-->`setcookie()`<!--markup:1:end-->** <!--markup:2:begin-->##setcookie()##<!--markup:2:end--> before setting new value
<!--markup:1:begin-->---
##### `nonce_index($action,<!--markup:1:end-->
<!--markup:2:begin-->----
====== ##nonce_index($action,<!--markup:2:end--> $code): <!--markup:1:begin-->string`<!--markup:1:end--> <!--markup:2:begin-->string##<!--markup:2:end--> (static) <!--markup:2:begin-->======<!--markup:2:end-->
Generate storage key for nonce.
**Returns:** <!--markup:1:begin-->`{action}.{base64_encoded_hash}`
---
---
##<!--markup:1:end-->** <!--markup:2:begin-->##{action}.{base64_encoded_hash}##
----
----
===<!--markup:2:end--> Session Lifecycle
<!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->===
====<!--markup:2:end--> Complete Session Flow
<!--markup:1:begin-->
`
<!--markup:2:end-->
┌─ Browser Request
│
├─ Application Code
│ └─ $session->start('appname')
│ │
│ ├─ Check if headers sent
│ ├─ Validate/read session name
│ ├─ Get session ID from:
│ │ 1. Parameter $id
│ │ 2. Cookie: {prefix}appname
│ ├─ Validate referer (if cf_referer_check set)
│ ├─ Validate ID format via store_validate_id()
│ ├─ store_open(name)
│ ├─ store_read(id)
│ │ └─ If missing/invalid/expired:
│ │ └─ set_new_id()
│ │ └─ regenerate_id = 2 (NEW)
│ ├─ Deserialize session data
│ ├─ exchangeArray(data)
│ ├─ active = true
│ ├─ cache_limiter()
│ │
│ └─ Security Checks (if NOT first request):
│ ├─ Verify NoReplay nonce
│ ├─ Check expiration flags
│ ├─ Check max session time
│ ├─ Check max idle time
│ ├─ Compare user agent (95%+ similarity)
│ ├─ Compare TLS status
│ ├─ Compare IP address
│ │ ├─ Match: OK
│ │ └─ Mismatch: destroy=1, regenerate
│ └─ Check regen time/probability
│ └─ regenerate_id()
│
├─ Application Code
│ └─ $session['key'] = 'value'
│
└─ End of Request
│
└─ register_shutdown_function() → terminator()
│
├─ Process flash data
│ └─ Decrement lifetimes
│ └─ Remove expired flash
├─ write_session()
│ └─ store_write(id, serialized_data)
├─ store_close()
├─ Probabilistic garbage collection
│ └─ store_gc() (cf_gc_probability % chance)
│ └─ Delete old sessions
└─ Output sent to browser
<!--markup:1:begin-->```
###<!--markup:1:end-->
<!--markup:2:begin-->
First Request (New Session)
`<!--markup:1:end--> <!--markup:2:begin-->==== %%<!--markup:2:end--> start() is called ├─ No ID in cookie ├─ store_read(id) → false ├─ set_new_id() │ └─ id = store_generate_id() │ └─ send_cookie(name, id) ├─ data = [] ├─ active = true ├─ populate() │ ├─ __started = now │ ├─ __regenerated = now │ ├─ __user_agent = UA │ └─ sticky__created = now └─ return true <!--markup:1:begin-->`
###
====<!--markup:2:end--> Subsequent Request (Resume Session) <!--markup:1:begin-->```<!--markup:1:end--> <!--markup:2:begin-->====start() is called
├─ ID from cookie
├─ store_read(id) → serialized_data
├─ data = unserialize(data)
├─ exchangeArray(data)
├─ active = true
├─ Security checks:
│ ├─ Replay check
│ ├─ Timeout checks
│ ├─ UA/IP/TLS checks
│ └─ May trigger regenerate_id()
└─ return true
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> Session ID Regeneration <!--markup:1:begin-->`
<!--markup:2:end--> regenerate_id($delete_old, $message) is called ├─ Check not headers_sent() ├─ Check $active ├─ Check not already regenerated in this request ├─ write_session() [Save current data] ├─ set __expire: │ ├─ if $delete_old=0: __expire = now + 5 │ └─ if $delete_old>0: __expire = 0 ├─ Generate new ID: │ └─ loop: │ ├─ id = store_generate_id() │ └─ while store_read(id) !== false [Ensure unique] ├─ Lock new session: store_read(id, true) ├─ Set: __regenerated = now ├─ Set: regenerated = 1 ├─ Log event: sticky__log[] = [now, message] └─ return true <!--markup:1:begin-->``` ###<!--markup:1:end--> <!--markup:2:begin-->
Session Destruction
`<!--markup:1:end--> <!--markup:2:begin-->==== %%<!--markup:2:end--> Triggered by: ├─ restart() → regenerate_id(true) ├─ Validation failure (destroy=2) │ └─ regenerate_id(2) │ └─ clean_vars() [Remove non-sticky data] └─ Timeout or security violation Results in: ├─ __expire = 0 [Immediate expiration] ├─ Non-sticky variables cleared ├─ sticky_ variables preserved └─ New session ID generated <!--markup:1:begin-->`
##
---- ===<!--markup:2:end--> Flash Data <!--markup:2:begin-->===<!--markup:2:end--> Flash data persists for a limited number of requests (typically 1-2) and is automatically removed. <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->====<!--markup:2:end--> Usage <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====php // Store flash message for next request
$session->set_flash('error', 'Username already exists', 1); // 1 request
$session->set_flash('info', 'Welcome back!', 2); // 2 requests
// In next request, data automatically available
echo $session['error']; // "Username already exists"
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> How It Works <!--markup:2:begin-->====<!--markup:2:end--> 1. **Storage:** Flash data stored in <!--markup:1:begin-->`$session->sticky__flash`<!--markup:1:end--> <!--markup:2:begin-->##$session->sticky__flash##<!--markup:2:end--> - Key: Variable name - Value: Lifetime in requests 2. **Cleanup:** In <!--markup:1:begin-->`terminator()`<!--markup:1:end--> <!--markup:2:begin-->##terminator()##<!--markup:2:end--> (shutdown handler): <!--markup:1:begin-->`php
php<!--markup:2:end-->
foreach ($sticky__flash as $var => $age) {
if (!isset($session[$var])) {
unset($sticky__flash[$var]); // Already deleted
} else if (--$age <= 0) {
unset($session[$var]); // Expired, remove
unset($flash__flash[$var]);
} else {
$flash__flash[$var] = $age; // Decrement counter
}
}
<!--markup:1:begin-->```<!--markup:1:end-->
<!--markup:2:begin-->
- Persistence: Flash variables are kept in
`sticky__flash`sticky__flasheven during session resets
==== Example: Login Flow
`php<!--markup:1:end--> <!--markup:2:begin-->====
%%php<!--markup:2:end-->
// POST /login
if ($credentials_valid) {
$session->restart(); // New session
$session['user_id'] = $user->id;
$session->set_flash('success', 'Login successful!', 1);
header('Location: /dashboard');
} else {
$session->set_flash('error', 'Invalid credentials', 1);
header('Location: /login');
}
// GET /dashboard (or /login on failure)
if ($message = $session['error'] ?? null) {
echo "<div class='error'>$message</div>";
}
if ($message = $session['success'] ?? null) {
echo "<div class='success'>$message</div>";
}
<!--markup:1:begin-->
`
##
---- ===<!--markup:2:end--> Nonce System <!--markup:2:begin-->===<!--markup:2:end--> Nonces provide CSRF protection and replay attack detection. <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->====<!--markup:2:end--> Terminology <!--markup:2:begin-->====<!--markup:2:end--> - **Nonce:** Number used ONCE - cryptographic token for action verification - **Action:** Type of operation being protected (e.g., 'form_submit', 'delete_user') - **Protected Nonce:** Can be verified multiple times with protection against rapid reuse <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->====<!--markup:2:end--> Complete Example: Form Protection <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====php // 1. Display form with nonce
$nonce = $session->create_nonce('user_update', 3600);
?>
<form method="POST" action="/update-profile">
<input type="hidden" name="nonce" value="<?= htmlspecialchars($nonce) ?>">
<input type="text" name="username" value="...">
<button type="submit">Update</button>
<input type="text" name="username" value="...">
<button type="submit">Update</button>
<?php
// 2. Process form submission
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$session->verify_nonce('user_update', $_POST['nonce'] ?? '')) {
}
http_response_code(403);
die('Security check failed');
die('Security check failed');
// Safe to process
update_user($_POST);
update_user($_POST);
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> Example: Protected Nonce (AJAX-Safe) <!--markup:1:begin-->`php
php<!--markup:2:end-->
// Generate protected nonce (can verify multiple times)
$nonce = $session->create_nonce('ajax_action', 300);
// Verify with protection level 3 (3 seconds)
$result = $session->verify_nonce('ajax_action', $_POST['nonce'], 3);
if ($result === -1) {
// Rapid reuse detected (possible attack, but might be AJAX)
if (is_ajax_request()) {
// AJAX is OK, disable replay protection this once
$session->cf_prevent_replay = 0;
} else {
// Likely attack
http_response_code(403);
die('Suspicious activity');
}
} else if ($result === true) {
// Safe to process
process_ajax();
}
<!--markup:1:begin-->```
###<!--markup:1:end-->
<!--markup:2:begin-->
Nonce Storage Format
`<!--markup:1:end--> <!--markup:2:begin-->====
%%<!--markup:2:end-->
Internal storage (__nonces array):
[
"{action}.{hash}" => expiration_timestamp,
"form_submit.AbCdEfGhIjK" => 1234567890,
"delete_user.XyZaBcDeFgH" => 1234567890,
]
Where:
- action: Custom action identifier
- hash: First 11 chars of base64(sha1(code_bytes))
- expiration_timestamp: time() + lifetime
<!--markup:1:begin-->
`
###
====<!--markup:2:end--> Security Properties <!--markup:2:begin-->====<!--markup:2:end--> - **CSRF Protection:** Nonce must match to process form - **One-Time Use:** Each nonce consumed after first verification (unless protected) - **Expiration:** Nonces automatically expire - **Action-Specific:** Each action has separate nonce space - **AJAX-Safe:** Protected nonces allow multiple quick verifications <!--markup:1:begin-->--- ##<!--markup:1:end--> <!--markup:2:begin-->---- ===<!--markup:2:end--> Cookie Management <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->=== ====<!--markup:2:end--> Security Features <!--markup:2:begin-->====<!--markup:2:end--> The <!--markup:1:begin-->`setcookie()`<!--markup:1:end--> <!--markup:2:begin-->##setcookie()##<!--markup:2:end--> method implements comprehensive cookie security: <!--markup:1:begin-->####<!--markup:1:end--> <!--markup:2:begin-->=====<!--markup:2:end--> Encoding <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->=====php // Cookie names: RFC 2616 2.2 token format
// Cookie values: RFC 6265 4.1.1 cookie-octet format
// Unsafe characters automatically URL-encoded
` ####<!--markup:1:end--> <!--markup:2:begin-->%% =====<!--markup:2:end--> Security Attributes <!--markup:1:begin-->`php
php<!--markup:2:end-->
setcookie('auth', 'token',
expires: time() + 3600,
secure: true, // HTTPS only
httponly: true, // Disable JavaScript
samesite: 'Strict' // CSRF protection
);
<!--markup:1:begin-->```
####<!--markup:1:end-->
<!--markup:2:begin-->
No Duplicate Headers
`php<!--markup:1:end--> <!--markup:2:begin-->===== %%php<!--markup:2:end--> // Automatically removes old Set-Cookie header before setting new one // Prevents cookie header duplication remove_cookie($name) → clears old headers setcookie() → sets new header <!--markup:1:begin-->`
###
====<!--markup:2:end--> Configuration-Driven Defaults <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====php $session->cf_cookie_path = '/app'; // Path
$session->cf_cookie_domain = '.example.com'; // Domain
$session->cf_cookie_secure = true; // HTTPS
$session->cf_cookie_httponly = true; // No JS
$session->cf_cookie_samesite = 'Lax'; // SameSite
$session->cf_cookie_prefix = 'app_'; // Prefix
$session->setcookie('token', 'value');
// Uses all configured defaults
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> Typical Secure Configuration <!--markup:1:begin-->`php
php<!--markup:2:end--> // Prevent XSS and CSRF $session->cf_cookie_secure = true; // HTTPS only $session->cf_cookie_httponly = true; // No JavaScript access $session->cf_cookie_samesite = 'Strict'; // Strict CSRF protection // Set scope $session->cf_cookie_path = '/'; // Root path $session->cf_cookie_domain = ''; // Current host only // Session cookies (delete on browser close) $session->cf_cookie_lifetime = 0; $session->cf_cookie_persistent = false; <!--markup:1:begin-->``` --- ##<!--markup:1:end--> <!--markup:2:begin-->
=== Error Handling
Graceful Degradation ====
The Session class gracefully handles errors:
===== Headers Already Sent
`php<!--markup:1:end--> <!--markup:2:begin-->=====
%%php<!--markup:2:end-->
if (headers_sent($file, $line)) {
trigger_error("id regeneration requested after headers flushed at $file:$line",
E_USER_WARNING);
return false;
}
<!--markup:1:begin-->
`<!--markup:2:end--> **Impact:** Session ID cannot be regenerated, but session continues <!--markup:1:begin-->####<!--markup:1:end--> <!--markup:2:begin-->=====<!--markup:2:end--> Cookie Setting Failure <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->=====php if (headers_sent($file, $line)) {
trigger_error("cannot place session cookie $name=$value due to $file:$line",
return;
E_USER_WARNING);
`<!--markup:1:end--> <!--markup:2:begin-->%%<!--markup:2:end--> **Impact:** Cookie not set, but session data remains accessible <!--markup:1:begin-->####<!--markup:1:end--> <!--markup:2:begin-->=====<!--markup:2:end--> Storage Errors <!--markup:1:begin-->`php
php<!--markup:2:end-->
if ($this->store_read($this->id, true) !== '') {
// error! [comment indicates error, but continues]
}
<!--markup:1:begin-->```<!--markup:1:end-->
<!--markup:2:begin-->
Impact: Creates new session if storage returns error
==== Debug Logging ====
The Session class includes commented debug statements:
`php<!--markup:1:end-->
<!--markup:2:begin-->%%php<!--markup:2:end-->
# Ut::dbg("regeneration failed by flush at $file:$line");
# Ut::dbg($destroy, $message);
# Ut::dbg("session setcookie $name failed by $file:$line");
<!--markup:1:begin-->
`<!--markup:2:end--> To enable: Uncomment lines and ensure <!--markup:1:begin-->`Ut::dbg()`<!--markup:1:end--> <!--markup:2:begin-->##Ut::dbg()##<!--markup:2:end--> function exists <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->====<!--markup:2:end--> Event Logging <!--markup:2:begin-->====<!--markup:2:end--> Session events tracked in <!--markup:1:begin-->`sticky__log`: ```php<!--markup:1:end--> <!--markup:2:begin-->##sticky__log##:php // Access session event history
if (isset($session->sticky__log)) {
foreach ($session->sticky__log as [$timestamp, $message]) {
}
echo "[$timestamp] $message\n";
`<!--markup:1:end--> <!--markup:2:begin-->%%<!--markup:2:end--> **Logged Events:** - Session regeneration (with reason) - Limited to 15 most recent events (old entries archived as '...') <!--markup:1:begin-->--- ##<!--markup:1:end--> <!--markup:2:begin-->---- ===<!--markup:2:end--> Implementation Guide <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->=== ====<!--markup:2:end--> Creating a Concrete Session Class <!--markup:2:begin-->====<!--markup:2:end--> You must implement the abstract storage methods. Choose your storage backend: files, database, cache, etc. <!--markup:1:begin-->####<!--markup:1:end--> <!--markup:2:begin-->=====<!--markup:2:end--> File-Based Storage <!--markup:1:begin-->`php
php<!--markup:2:end-->
<?php
class FileSession extends Session {
private $session_dir = '/tmp/sessions';
private $file_handle = null;
public function __construct() {
parent::__construct();
if (!is_dir($this->session_dir)) {
mkdir($this->session_dir, 0700, true);
}
}
protected function store_open($name): void {
// PHP sessions don't really "open", just prepare
// In file mode, we could initialize directory
}
protected function store_read($id, $lock = false): string|false {
$file = $this->session_dir . '/sess_' . preg_replace('/[^a-zA-Z0-9]/', '', $id);
if (!file_exists($file)) {
if ($lock) {
// Create new session file
file_put_contents($file, '', LOCK_EX);
return '';
}
return false;
}
if (filemtime($file) < time() - $this->cf_gc_maxlifetime) {
unlink($file); // Expired
return false;
}
return file_get_contents($file);
}
protected function store_write($id, $data): void {
$file = $this->session_dir . '/sess_' . preg_replace('/[^a-zA-Z0-9]/', '', $id);
file_put_contents($file, $data, LOCK_EX);
}
protected function store_close(): void {
// No cleanup needed for file backend
}
protected function store_gc(): void {
$cutoff = time() - $this->cf_gc_maxlifetime;
foreach (glob($this->session_dir . '/sess_*') as $file) {
if (filemtime($file) < $cutoff) {
unlink($file);
}
}
}
}
<!--markup:1:begin-->```
####<!--markup:1:end-->
<!--markup:2:begin-->
Database Storage (PDO)
`php<!--markup:1:end--> <!--markup:2:begin-->=====
%%php<!--markup:2:end-->
<?php
class DatabaseSession extends Session {
private PDO $pdo;
public function __construct(PDO $pdo) {
parent::__construct();
$this->pdo = $pdo;
$this->ensure_table();
}
private function ensure_table(): void {
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS sessions (
id VARCHAR(21) PRIMARY KEY,
data LONGTEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
)
SQL;
$this->pdo->exec($sql);
}
protected function store_open($name): void {
// Database already connected
}
protected function store_read($id, $lock = false): string|false {
$stmt = $this->pdo->prepare('SELECT data FROM sessions WHERE id = ?');
$stmt->execute([$id]);
if ($result = $stmt->fetch(PDO::FETCH_ASSOC)) {
return $result['data'];
}
if ($lock) {
// Create new session
$stmt = $this->pdo->prepare('INSERT INTO sessions (id, data) VALUES (?, ?)');
$stmt->execute([$id, '']);
return '';
}
return false;
}
protected function store_write($id, $data): void {
$stmt = $this->pdo->prepare(
'INSERT INTO sessions (id, data) VALUES (?, ?)
ON DUPLICATE KEY UPDATE data = VALUES(data)'
);
$stmt->execute([$id, $data]);
}
protected function store_close(): void {
// Connection persists for application
}
protected function store_gc(): void {
$cutoff = time() - $this->cf_gc_maxlifetime;
$this->pdo->prepare('DELETE FROM sessions WHERE updated_at < FROM_UNIXTIME(?)')
->execute([$cutoff]);
}
}
<!--markup:1:begin-->
`
=====<!--markup:2:end--> Redis Storage <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->=====php <?php
class RedisSession extends Session {
private Redis $redis;
private string $prefix = 'sess:';
private string $prefix = 'sess:';
public function __construct(Redis $redis) {
}
parent::__construct();
$this->redis = $redis;
$this->redis = $redis;
protected function store_open($name): void {
}
// Redis already connected
protected function store_read($id, $lock = false): string|false {
$data = $this->redis->get($this->prefix . $id);
if ($data !== false) {
}
return $data;
if ($lock) {
}
// Create new session
$this->redis->set($this->prefix . $id, '',
return '';
$this->redis->set($this->prefix . $id, '',
['EX' => $this->cf_gc_maxlifetime]);
return false;
protected function store_write($id, $data): void {
}
$this->redis->set($this->prefix . $id, $data,
['EX' => $this->cf_gc_maxlifetime]);
protected function store_close(): void {
}
// Connection persists
protected function store_gc(): void {
}
// Redis handles expiration automatically with TTL
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> Complete Integration Example <!--markup:1:begin-->`php
php<!--markup:2:end-->
<?php
// Initialize session with configuration
$session = new FileSession();
// Configure security
$session->cf_cookie_secure = (!empty($_SERVER['HTTPS']));
$session->cf_cookie_httponly = true;
$session->cf_cookie_samesite = 'Lax';
$session->cf_max_session = 86400; // 24 hours
$session->cf_max_idle = 3600; // 1 hour
$session->cf_prevent_replay = true;
// Set IP and TLS validation
$session->cf_ip = $_SERVER['REMOTE_ADDR'];
$session->cf_tls = !empty($_SERVER['HTTPS']);
// Start session
if (!$session->start('myapp')) {
die('Session start failed');
}
// Check for session validation messages
if ($message = $session->message()) {
error_log("Session validation: $message");
}
// Use session
if (!isset($session['user_id'])) {
// Handle login...
$session['user_id'] = $user->id;
$session['username'] = $user->name;
$session->regenerate_id(false, 'login');
} else {
// User already logged in
echo "Welcome back, " . htmlspecialchars($session['username']);
}
// Logout handling
if ($_REQUEST['action'] === 'logout') {
$session->restart();
header('Location: /');
}
// Automatic cleanup happens in register_shutdown_function()
<!--markup:1:begin-->```
###<!--markup:1:end-->
<!--markup:2:begin-->
Configuration Best Practices
`php<!--markup:1:end--> <!--markup:2:begin-->====
%%php<!--markup:2:end-->
<?php
class SessionConfig {
public static function apply(Session $session, string $environment = 'production'): void {
// Base configuration
$session->cf_cookie_prefix = 'app_';
$session->cf_cookie_path = '/';
$session->cf_cache_limiter = 'private';
if ($environment === 'production') {
// Strict production settings
$session->cf_cookie_secure = true; // HTTPS only
$session->cf_cookie_httponly = true; // No JavaScript
$session->cf_cookie_samesite = 'Strict'; // Maximum CSRF protection
$session->cf_prevent_replay = true; // Anti-replay
$session->cf_max_session = 3600; // 1 hour
$session->cf_max_idle = 1800; // 30 minutes
$session->cf_regen_time = 300; // Regen every 5 min
$session->cf_regen_probability = 50; // 50% chance
} else {
// Development settings
$session->cf_cookie_secure = false; // Allow HTTP
$session->cf_cookie_httponly = false; // Allow JS debugging
$session->cf_prevent_replay = false; // Easier testing
$session->cf_max_session = 86400; // 24 hours
$session->cf_max_idle = 3600; // 1 hour
$session->cf_regen_time = 60; // 1 minute
$session->cf_regen_probability = 10; // 10% chance
}
}
}
// Usage
$session = new FileSession();
SessionConfig::apply($session, $_ENV['APP_ENV'] ?? 'production');
$session->start('myapp');
<!--markup:1:begin-->
`
###
====<!--markup:2:end--> Testing Tips <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====php <?php
// Test nonce generation and verification
$nonce1 = $session->create_nonce('test_action', 60);
assert($session->verify_nonce('test_action', $nonce1) === true);
// Test single-use property
assert($session->verify_nonce('test_action', $nonce1) === false);
// Test expiration
$old_nonce = $session->create_nonce('expire_test', 1);
sleep(2);
assert($session->verify_nonce('expire_test', $old_nonce) === false);
// Test user agent validation
assert(isset($session->__user_agent));
// Test session ID format
assert(preg_match('/^[a-zA-Z0-9]{21}$/', $session->id()));
// Test data persistence
$session['test_key'] = 'test_value';
$session->write_close();
// New request...
$session2 = new FileSession();
$session2->start('myapp');
assert($session2['test_key'] === 'test_value');
` --- ##<!--markup:1:end--> <!--markup:2:begin-->%% ---- ===<!--markup:2:end--> Security Checklist <!--markup:2:begin-->===<!--markup:2:end--> Use this checklist when implementing sessions: - [ ] Use HTTPS only in production - [ ] Enable <!--markup:1:begin-->`cf_cookie_secure`<!--markup:1:end--> <!--markup:2:begin-->##cf_cookie_secure##<!--markup:2:end--> - [ ] Enable <!--markup:1:begin-->`cf_cookie_httponly`<!--markup:1:end--> <!--markup:2:begin-->##cf_cookie_httponly##<!--markup:2:end--> - [ ] Set <!--markup:1:begin-->`cf_cookie_samesite`<!--markup:1:end--> <!--markup:2:begin-->##cf_cookie_samesite##<!--markup:2:end--> to 'Strict' or 'Lax' - [ ] Set appropriate <!--markup:1:begin-->`cf_max_session`<!--markup:1:end--> <!--markup:2:begin-->##cf_max_session##<!--markup:2:end--> timeout - [ ] Set appropriate <!--markup:1:begin-->`cf_max_idle`<!--markup:1:end--> <!--markup:2:begin-->##cf_max_idle##<!--markup:2:end--> timeout - [ ] Enable <!--markup:1:begin-->`cf_prevent_replay`<!--markup:1:end--> <!--markup:2:begin-->##cf_prevent_replay##<!--markup:2:end--> - [ ] Validate <!--markup:1:begin-->`cf_ip`<!--markup:1:end--> <!--markup:2:begin-->##cf_ip##<!--markup:2:end--> if possible - [ ] Validate <!--markup:1:begin-->`cf_tls`<!--markup:1:end--> <!--markup:2:begin-->##cf_tls##<!--markup:2:end--> on HTTPS sites - [ ] Use nonces for all state-changing forms - [ ] Implement proper logout (call <!--markup:1:begin-->`restart()`)<!--markup:1:end--> <!--markup:2:begin-->##restart()##)<!--markup:2:end--> - [ ] Regenerate on privilege escalation (login) - [ ] Monitor <!--markup:1:begin-->`sticky__ip`<!--markup:1:end--> <!--markup:2:begin-->##sticky__ip##<!--markup:2:end--> for suspicious changes - [ ] Review <!--markup:1:begin-->`sticky__log`<!--markup:1:end--> <!--markup:2:begin-->##sticky__log##<!--markup:2:end--> for attack patterns - [ ] Implement garbage collection <!--markup:1:begin-->(`store_gc`)<!--markup:1:end--> <!--markup:2:begin-->(##store_gc##)<!--markup:2:end--> - [ ] Hash session IDs before storing (see TODOs) - [ ] Use secure random token generation <!--markup:1:begin-->--- ##<!--markup:1:end--> <!--markup:2:begin-->---- ===<!--markup:2:end--> Common Patterns <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->=== ====<!--markup:2:end--> Login Flow <!--markup:1:begin-->`php
php<!--markup:2:end-->
if ($_POST['action'] === 'login') {
$user = authenticate($_POST['username'], $_POST['password']);
if ($user) {
$session->regenerate_id(false, 'login'); // New ID after auth
$session['user_id'] = $user->id;
$session['username'] = $user->username;
$session['roles'] = $user->roles;
header('Location: /dashboard');
} else {
$session->set_flash('error', 'Invalid credentials', 1);
header('Location: /login');
}
}
<!--markup:1:begin-->```
###<!--markup:1:end-->
<!--markup:2:begin-->
Logout Flow
`php<!--markup:1:end--> <!--markup:2:begin-->====
%%php<!--markup:2:end-->
if ($_GET['action'] === 'logout') {
$session->restart(); // Complete reset
header('Location: /');
}
<!--markup:1:begin-->
`
###
====<!--markup:2:end--> CSRF-Protected Form <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====php // Display form
$csrf = $session->create_nonce('form_' . $form_id, 3600);
echo '<form method="POST">';
echo '<input type="hidden" name="csrf" value="' . htmlspecialchars($csrf) . '">';
// ... form fields
echo '</form>';
// Process form
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$session->verify_nonce('form_' . $form_id, $_POST['csrf'] ?? '')) {
}
// Process safely
die('CSRF check failed');
// Process safely
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> Permission Check with Session Regeneration <!--markup:1:begin-->`php
php<!--markup:2:end-->
if ($user->privilege_level < ADMIN_LEVEL && $promoted_to_admin) {
$session->regenerate_id(false, 'privilege_escalation');
$session['is_admin'] = true;
}
<!--markup:1:begin-->```
###<!--markup:1:end-->
<!--markup:2:begin-->
Session Messages/Flash
`php<!--markup:1:end--> <!--markup:2:begin-->====
%%php<!--markup:2:end-->
// After action
$session->set_flash('info', 'Profile updated successfully', 1);
// Display next page
if (isset($session['info'])) {
echo $session['info'];
}
<!--markup:1:begin-->
`
##
---- ===<!--markup:2:end--> Performance Considerations <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->=== ====<!--markup:2:end--> Optimization Tips <!--markup:2:begin-->====<!--markup:2:end--> 1. **Minimize Session Writes:** - Session data only written during <!--markup:1:begin-->`write_close()`<!--markup:1:end--> <!--markup:2:begin-->##write_close()##<!--markup:2:end--> or regeneration - No unnecessary serialization during reads 2. **Garbage Collection:** - Probabilistic GC (based on <!--markup:1:begin-->`cf_gc_probability`)<!--markup:1:end--> <!--markup:2:begin-->##cf_gc_probability##)<!--markup:2:end--> - Only runs on ~2% of requests by default - Customize based on your session volume 3. **Nonce Cleanup:** - Expired nonces automatically removed on verification - Verified nonces removed from storage - No manual cleanup needed 4. **Session ID Validation:** - Regex-based validation is fast - No database lookup needed 5. **Caching Strategy:** - Cache expensive lookups between session operations - Session data loaded once per request <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->====<!--markup:2:end--> Benchmarks <!--markup:2:begin-->====<!--markup:2:end--> Typical performance on modern hardware: - Session start: ~1-5ms (file) / ~2-10ms (database) - Session write: <1ms (file) / 1-5ms (database) - Nonce generation: <1ms - Nonce verification: <1ms <!--markup:1:begin-->--- ##<!--markup:1:end--> <!--markup:2:begin-->---- ===<!--markup:2:end--> Troubleshooting <!--markup:1:begin-->###<!--markup:1:end--> <!--markup:2:begin-->=== ====<!--markup:2:end--> Session Not Starting <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====php if (!$session->start('myapp')) {
// Check reasons:
// 1. Headers already sent?
// 2. Storage backend not initialized?
// 3. Permissions issue on session directory?
debug_backtrace();
// 1. Headers already sent?
// 2. Storage backend not initialized?
// 3. Permissions issue on session directory?
debug_backtrace();
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> Cookie Not Setting <!--markup:1:begin-->`php
php<!--markup:2:end--> // If setcookie() returns false: // - Check if headers_sent() // - Check if cookie name is RFC 2616 compliant // - Check if cookie value is properly encoded <!--markup:1:begin-->``` ###<!--markup:1:end--> <!--markup:2:begin-->
Session ID Not Regenerating
`php<!--markup:1:end--> <!--markup:2:begin-->====
%%php<!--markup:2:end-->
// If regenerate_id() returns false:
// - Headers might be sent
// - $active might be false
// - Already regenerated once in this request
if (!$session->regenerate_id()) {
error_log("Regeneration failed: headers sent or session inactive");
}
<!--markup:1:begin-->
`
###
====<!--markup:2:end--> Nonce Verification Failing <!--markup:1:begin-->```php<!--markup:1:end--> <!--markup:2:begin-->====php // If verify_nonce() returns false:
// 1. Nonce might be expired
// 2. Nonce might be for different action
// 3. Nonce might have been used already
// 4. Session might have been reset
// Debug:
var_dump($session->__nonces); // See stored nonces
` ###<!--markup:1:end--> <!--markup:2:begin-->%% ====<!--markup:2:end--> Session Data Lost <!--markup:1:begin-->`php
php<!--markup:2:end-->
// Possible causes:
// 1. write_close() not called (usually automatic via shutdown)
// 2. Storage backend failing silently
// 3. File permissions issues
// 4. Session timeout due to cf_max_idle
// 5. IP/UA/TLS validation failure (check message())
if ($message = $session->message()) {
error_log("Session issue: $message");
}
<!--markup:1:begin-->```
---
##<!--markup:1:end-->
<!--markup:2:begin-->
=== TODO Items (From Code Comments) ===
The following improvements are planned:
- Do not store session ID in filename or DB index - store hash instead
- Improves security by not exposing IDs in storage layer
- Would require hashing logic in store_* methods
- Log of IP changes and other possible security alerts
- Track
`sticky__ip`sticky__ipchanges more comprehensively - Create security audit trail
- Allocate internal unique session which lives through lifetime of uber-session
- Multi-session management (parent/child sessions)
- Useful for complex user flows
- Do not delete old sessions, but use them as hijack pointers
- Maintain session history for analysis
- Detect potential session hijacking patterns
- Implement session relationship tracking
- All SIDs used later than 5secs of regenerations is hijacks
- Detect and block delayed session ID usage
- Current implementation allows 5-second window
- Could be more granular
##
=== References
Security Standards ====
- RFC 2616: HTTP/1.1 (Cookie syntax)
- RFC 6265: HTTP State Management Mechanism
- RFC 6234: US Secure Hash and Message Authentication Code Algorithms
- OWASP: Session Management Cheat Sheet
- OWASP: Cross-Site Request Forgery (CSRF) Prevention
==== Related Code ====
-
`Ut::serialize()`Ut::serialize()/`Ut::unserialize()`:Ut::unserialize(): Session data serialization -
`Ut::random_token()`:Ut::random_token(): Cryptographic token generation -
`Ut::http_date()`:Ut::http_date(): HTTP date formatting -
`Ut::urlencode()`:Ut::urlencode(): Cookie-safe encoding -
`Ut::is_empty()`:Ut::is_empty(): Empty value checking
==== See Also ====
-
`src/class/http.php`:src/class/http.php: HTTP request/response handling -
`src/class/auth.php`:src/class/auth.php: Authentication (uses Session) - Session security best practices in OWASP documentation
##
=== Version History ===
- Current: Abstract session class with security features
- Planned: Implementation of TODO items above
- Documentation generated: 2026-05-05*
- For latest updates, see: https://github.com/Trojer/wack[...]ION_DOCUMENTATION.md*