WackoWiki: Session Management - Technical Documentation

https://wackowiki.org/doc     Version: 20 (05/05/2026 21:05)

Session Management - Technical Documentation


1. 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

2. Core Concepts

2.1. Session State

The Session class maintains three primary states:

2.2. Session Data Storage

Session data is stored as an array accessible through ArrayObject interface:
$session['user_id'] = 123;  // Set data
echo $session['user_id'];   // Get data

2.3. Sticky Data

Variables prefixed with sticky_ are persistent across session resets:

2.4. Internal Tracking Variables

Variables prefixed with __ are internal session metadata:

3. Architecture

3.1. Class Hierarchy

ArrayObject (PHP native)
    ↓
Session (abstract)
    ↓
[Concrete Implementation] (must implement store_* methods)	

3.2. Key Methods Categories


Lifecycle Management:

Security:

Storage (Abstract – Must Implement):

Cookie Management:

4. Configuration

4.1. Configuration Properties (Public)


All configuration properties are prefixed with cf_ (config) and can be set before calling start():

4.1.1. Session Behavior

$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)

4.1.2. Nonce & Replay Protection

$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

4.1.3. Garbage Collection

$session->cf_gc_probability = 2;           // Probability of GC on shutdown (0-100)
$session->cf_gc_maxlifetime = 1440;        // Max session file lifetime (seconds)

4.1.4. Cookie Settings

$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

4.1.5. Cache Control

$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

4.1.6. Security Validation

$session->cf_referer_check = '';           // Check HTTP Referer header

4.1.7. HTTP Context (Set by HTTP class)

$session->cf_ip;                           // Client IP address
$session->cf_tls;                          // TLS/SSL connection indicator

5. Usage

5.1. Basic Session Setup


// 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()

5.2. Session Data Access


// 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();

5.3. Session ID Management


// 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);

5.4. Session State


// 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();

6. Security Features

6.1. Session ID Regeneration


Purpose: Prevent session fixation attacks

Automatic Triggers:

Manual Trigger:
$session->regenerate_id($delete_old = false, $message = 'custom_reason');


Parameters:

Implementation Details:

// Example: Force regeneration on login
$session->start('myapp');
if ($user_authenticated) {
    $session->regenerate_id(false, 'login');
    $session['user_id'] = $user->id;
}

6.2. User Agent Validation


Purpose: Detect browser/device changes that might indicate hijacking

Behavior:

Configuration:
// Automatic on each request (if enabled in code logic)
// Triggers session destruction if UA changes significantly

6.3. IP Address Validation


Purpose: Detect IP spoofing or hijacking

Behavior:

Configuration:
$session->cf_ip = $_SERVER['REMOTE_ADDR'];  // Set by HTTP class
// Validation happens automatically during start()


IP Change Tracking:
// Access IP change history
$ip_history = $session->sticky__ip;  // Array of [ip => change_count]

6.4. TLS/SSL Validation


Purpose: Prevent protocol downgrade attacks

Behavior:

Configuration:
$session->cf_tls = !empty($_SERVER['HTTPS']);  // Set by HTTP class
// Validation happens automatically during start()

6.5. Anti-Replay Protection


Purpose: Prevent CSRF and replay attacks

Mechanism:

Configuration:
$session->cf_prevent_replay = 1;       // Enable (default)
$session->cf_prevent_replay = 0;       // Disable if needed


How It Works:
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)	

6.6. Referer Validation (Optional)


Purpose: Prevent CSRF via header checking

Configuration:
$session->cf_referer_check = 'example.com';
// Session rejected if HTTP_REFERER doesn't contain this string

7. API Reference

7.1. Public Methods

7.1.1. Lifecycle Management

7.1.1.1. start($name = null, $id = null): bool
Start or resume a session.

Parameters:

Returns: true if session started successfully, false on error

Side Effects:

Example:
if ($session->start('webapp', $_COOKIE['sess_id'] ?? null)) {
    // Session ready
} else {
    // Session failed
}


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 store_validate_id()
  6. Read session data from storage
  7. Verify nonces and timestamps
  8. Check user agent, IP, TLS 
  9. Regenerate if needed
7.1.1.2. write_close(): void
Save session data and close session.

Side Effects:

Example:
$session['key'] = 'value';
$session->write_close();  // Ensure data is saved

7.1.1.3. restart(): bool
Destroy current session and create new one.

Equivalent to: regenerate_id(true) + clean_vars() + populate()

Returns: true on success, false on error

Use Cases:

Example:
$session->restart();
// New session created, old data cleared, sticky_ vars preserved

7.1.2. Session Access

7.1.2.1. id(): mixed
Get current session ID.

Returns: Session ID string or null if not started

$sid = $session->id();  // "abc123xyz..."

7.1.2.2. name(): string
Get session name (cookie prefix).

Returns: Session name

$name = $session->name();  // "myapp"

7.1.2.3. active(): bool
Check if session is currently active.

Returns: true if session is started and active, false otherwise

if ($session->active()) {
    $session['key'] = 'value';
}

7.1.2.4. message(): string|null
Get reason for last session state change.

Returns: Message string or null

Possible Values:

Example:
$session->start('app');
if ($message = $session->message()) {
    error_log("Session issue: $message");
}

7.1.2.5. toArray(): array
Convert session data to array.

Returns: Associative array of session data

Note: This is a direct call to ArrayObject::getArrayCopy()

$data = $session->toArray();
foreach ($data as $key => $value) {
    echo "$key => $value\n";
}

7.1.3. Nonce System

7.1.3.1. create_nonce($action, $expires = null): string
Generate a unique nonce token.

Parameters:

Returns: Nonce token string (11 characters)

Example:
$nonce = $session->create_nonce('form_submit', 3600);
// Use in HTML: <input type="hidden" name="nonce" value="<?= $nonce ?>">


Storage:
7.1.3.2. verify_nonce($action, $code, $protect = 0)
Verify a nonce token.

Parameters:

Returns:

Example:
if ($nonce = $session->verify_nonce('form_submit', $_POST['nonce'])) {
    if ($nonce === -1) {
        // Possible replay, but might be legitimate AJAX
        $session->cf_prevent_replay = 0;  // Disable for this request
    } else {
        // Safe to process
        process_form();
    }
}


Cleanup:

7.1.4. Cookie Management

7.1.4.1. setcookie($name, $value = null, $expires = 0, $path = null, $domain = null, $secure = null, $httponly = null, $samesite = null): bool
Set a cookie with security headers.

Parameters:

Returns: true on success, false if headers already sent

Features:

Example:
// 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');

7.1.4.2. get_cookie($name)
Retrieve cookie value.

Parameters:

Returns: Cookie value or null if not set

$value = $session->get_cookie('user_pref');  // Reads $_COOKIE['user_pref']

7.1.4.3. set_cookie($name, $value, $persistent = false): void
Legacy cookie setter (alternative to setcookie()).

Parameters:

Example:
$session->set_cookie('theme', 'dark');  // Session cookie
$session->set_cookie('lang', 'en', 365);  // 1 year

7.1.4.4. delete_cookie($name): void
Delete a cookie.

Parameters:

Implementation: Sets empty value with immediate expiration

$session->delete_cookie('old_preference');

7.1.4.5. unsetcookie($name): void
Alias for setcookie($name) with no value (convenience method).

$session->unsetcookie('cookie_name');

7.2. Protected Methods (For Store Implementation)

7.2.1. regenerate_id($delete_old = false, $message = ''): bool

Internal method to regenerate session ID (called automatically).

Protected – Usually called automatically, but can be overridden/called by subclasses

7.2.2. store_generate_id(): string

Generate a new session ID.

Default Implementation: Returns 21-character random alphanumeric string via Ut::random_token(21)

Override in subclass to customize:
protected function store_generate_id(): string {
    return hash('sha256', random_bytes(32));  // Your format
}

7.2.3. store_validate_id($id): bool

Validate session ID format.

Default Implementation: Regex check: /^[a-zA-Z\d]{21}$/

Override in subclass to match your format:
protected function store_validate_id($id): bool {
    return preg_match('/^[a-f0-9]{64}$/', $id);  // SHA256 format
}

7.2.4. store_open($name): void

Open session storage (called before first read/write).

Subclass must implement – Initialize storage handler

Example:
php
protected function store_open($name): void {
    $this->db = new PDO('sqlite::memory:');
}	

7.2.5. store_read($id, $lock = false): string|false

Read session data from storage.

Subclass must implement

Parameters:

Returns:

Example:
php
protected function store_read($id, $lock = false): string|false {
    $data = file_get_contents("/tmp/sess_$id");
    return $data ?: false;
}	

7.2.6. store_write($id, $data): void

Write session data to storage.

Subclass must implement

Parameters:

Example:
php
protected function store_write($id, $data): void {
    file_put_contents("/tmp/sess_$id", $data);
}	

7.2.7. store_close(): void

Close session storage.

Subclass must implement – Release resources

Example:
php
protected function store_close(): void {
    // Close database, file, etc.
}	

7.2.8. store_gc(): void

Perform garbage collection on old sessions.

Subclass must implement – Delete expired sessions

Called During:

Should Delete:

Example:
php
protected function store_gc(): void {
    $max_age = time() - $this->cf_gc_maxlifetime;
    // Delete files/records older than $max_age
}	

7.3. Private Methods (Internal Use)

7.3.1.1. populate(): void
Initialize session tracking variables on first request.

Called by: start(), restart()

Initializes:
7.3.1.2. write_session(): void
Serialize and write session data to storage.

Called by: regenerate_id(), write_close(), terminator()

Updates:
7.3.1.3. clean_vars(): void
Remove non-sticky session variables.

Called by: restart(), session validation failure

Preserves: Variables starting with sticky_
7.3.1.4. prevent_replay(): void
Generate and send anti-replay nonce.

Called by: populate()

Action:
7.3.1.5. cache_limiter(): void
Set HTTP cache control headers based on configuration.

Called by: start() after session data loaded

Modes:
7.3.1.6. set_new_id(): void
Generate and assign new session ID, send in cookie.

Called by: regenerate_id(), start() (for new sessions)
7.3.1.7. remove_cookie($cookie): void
Remove existing Set-Cookie header to avoid duplicates.

Called by: setcookie() before setting new value
7.3.1.8. nonce_index($action, $code): string (static)
Generate storage key for nonce.

Returns: {action}.{base64_encoded_hash}

8. Session Lifecycle

8.1. Complete Session Flow


┌─ 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	

8.2. First Request (New Session)


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	

8.3. Subsequent Request (Resume Session)


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	

8.4. Session ID Regeneration


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	

8.5. Session Destruction


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	

9. Flash Data


Flash data persists for a limited number of requests (typically 1–2) and is automatically removed.

9.1. Usage


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"	

9.2. How It Works

  1. Storage: Flash data stored in $session->sticky__flash
    • Key: Variable name
    • Value: Lifetime in requests
  2. Cleanup: In terminator() (shutdown handler):
    php
       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
           }
       }	

  3. Persistence: Flash variables are kept in sticky__flash even during session resets

9.3. Example: Login Flow


php
// 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>";
}	

10. Nonce System


Nonces provide CSRF protection and replay attack detection.

10.1. Terminology

10.2. Complete Example: Form Protection


// 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>
</form>

<?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');
    }

    // Safe to process
    update_user($_POST);
}

10.3. Example: Protected Nonce (AJAX-Safe)


// 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();
}

10.4. Nonce Storage Format


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	

10.5. Security Properties

11. Cookie Management

11.1. Security Features


The setcookie() method implements comprehensive cookie security:

11.1.1. Encoding

php
// Cookie names: RFC 2616 2.2 token format
// Cookie values: RFC 6265 4.1.1 cookie-octet format
// Unsafe characters automatically URL-encoded	

11.1.2. Security Attributes

php
setcookie('auth', 'token',
    expires: time() + 3600,
    secure: true,           // HTTPS only
    httponly: true,         // Disable JavaScript
    samesite: 'Strict'      // CSRF protection
);	

11.1.3. No Duplicate Headers

php
// Automatically removes old Set-Cookie header before setting new one
// Prevents cookie header duplication
remove_cookie($name) → clears old headers
setcookie() → sets new header	

11.2. Configuration-Driven Defaults


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	

11.3. Typical Secure Configuration


php
// 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;	

12. Error Handling

12.1. Graceful Degradation


The Session class gracefully handles errors:

12.1.1. Headers Already Sent

php
if (headers_sent($file, $line)) {
    trigger_error("id regeneration requested after headers flushed at $file:$line", 
                  E_USER_WARNING);
    return false;
}	


Impact: Session ID cannot be regenerated, but session continues

12.1.2. Cookie Setting Failure

php
if (headers_sent($file, $line)) {
    trigger_error("cannot place session cookie $name=$value due to $file:$line", 
                  E_USER_WARNING);
    return;
}	


Impact: Cookie not set, but session data remains accessible

12.1.3. Storage Errors

php
if ($this->store_read($this->id, true) !== '') {
    // error!  [comment indicates error, but continues]
}	


Impact: Creates new session if storage returns error

12.2. Debug Logging


The Session class includes commented debug statements:

php
# Ut::dbg("regeneration failed by flush at $file:$line");
# Ut::dbg($destroy, $message);
# Ut::dbg("session setcookie $name failed by $file:$line");	


To enable: Uncomment lines and ensure Ut::dbg() function exists

12.3. Event Logging


Session events tracked in sticky__log:

php
// Access session event history
if (isset($session->sticky__log)) {
    foreach ($session->sticky__log as [$timestamp, $message]) {
        echo "[$timestamp] $message\n";
    }
}	


Logged Events:

13. Implementation Guide

13.1. Creating a Concrete Session Class


You must implement the abstract storage methods. Choose your storage backend: files, database, cache, etc.

13.1.1. File-Based Storage


<?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);
            }
        }
    }
}

13.1.2. Database Storage (PDO)


<?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]);
    }
}

13.1.3. Redis Storage


<?php

class RedisSession extends Session {
    private Redis $redis;
    private string $prefix = 'sess:';

    public function __construct(Redis $redis) {
        parent::__construct();
        $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, '', 
                            ['EX' => $this->cf_gc_maxlifetime]);
            return '';
        }

        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
    }
}

13.2. Complete Integration Example


<?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()

13.3. Configuration Best Practices


<?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');

13.4. Testing Tips


<?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');

14. Security Checklist


Use this checklist when implementing sessions:

15. Common Patterns

15.1. Login Flow


php
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');
    }
}	

15.2. Logout Flow


php
if ($_GET['action'] === 'logout') {
    $session->restart();  // Complete reset
    header('Location: /');
}	

15.3. CSRF-Protected Form


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'] ?? '')) {
        die('CSRF check failed');
    }
    // Process safely
}	

15.4. Permission Check with Session Regeneration


php
if ($user->privilege_level < ADMIN_LEVEL && $promoted_to_admin) {
    $session->regenerate_id(false, 'privilege_escalation');
    $session['is_admin'] = true;
}	

15.5. Session Messages/Flash


php
// After action
$session->set_flash('info', 'Profile updated successfully', 1);

// Display next page
if (isset($session['info'])) {
    echo $session['info'];
}	

16. Performance Considerations

16.1. Optimization Tips

  1. Minimize Session Writes:
    • Session data only written during write_close() or regeneration
    • No unnecessary serialization during reads
  2. Garbage Collection:
    • Probabilistic GC (based on cf_gc_probability)
    • 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

16.2. Benchmarks


Typical performance on modern hardware:

17. Troubleshooting

17.1. Session Not Starting


php
if (!$session->start('myapp')) {
    // Check reasons:
    // 1. Headers already sent?
    // 2. Storage backend not initialized?
    // 3. Permissions issue on session directory?
    debug_backtrace();
}	

17.2. Cookie Not Setting


php
// If setcookie() returns false:
// - Check if headers_sent()
// - Check if cookie name is RFC 2616 compliant
// - Check if cookie value is properly encoded	

17.3. Session ID Not Regenerating


php
// 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");
}	

17.4. Nonce Verification Failing


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	

17.5. Session Data Lost


php
// 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");
}	

18. TODO Items (From Code Comments)

The following improvements are planned:
  1. 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
  2. Log of IP changes and other possible security alerts
    • Track sticky__ip changes more comprehensively
    • Create security audit trail
  3. Allocate internal unique session which lives through lifetime of uber-session
    • Multi-session management (parent/child sessions)
    • Useful for complex user flows
  4. 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
  5. 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

19. References

19.1. Security Standards

19.2. Related Code

19.3. See Also

20. Version History