Difference between revisions for Users / Eo Ny





Next edit →

Version1 Version2
1 **((user:EoNy EoNy))** (05.05.2026 16:15) 1 # Session Management Technical Documentation
    2
    3 ## Overview
    4
    5 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.
    6
    7 **Location:** `src/class/session.php`
    8 **Type:** Abstract class (must be extended with a `SessionStoreInterface` implementation)
    9 **Inheritance:** `ArrayObject`
    10
    11 ---
    12
    13 ## Table of Contents
    14
    15 1. [Core Concepts](#core-concepts)
    16 2. [Architecture](#architecture)
    17 3. [Configuration](#configuration)
    18 4. [Usage](#usage)
    19 5. [Security Features](#security-features)
    20 6. [API Reference](#api-reference)
    21 7. [Session Lifecycle](#session-lifecycle)
    22 8. [Flash Data](#flash-data)
    23 9. [Nonce System](#nonce-system)
    24 10. [Cookie Management](#cookie-management)
    25 11. [Error Handling](#error-handling)
    26 12. [Implementation Guide](#implementation-guide)
    27
    28 ---
    29
    30 ## Core Concepts
    31
    32 ### Session State
    33 The Session class maintains three primary states:
    34
    35 - **Inactive** (`$active = false`): Session not yet started or has been closed
    36 - **Active** (`$active = true`): Session is running and can store/retrieve data
    37 - **Regenerated**: Session ID has been replaced (tracked via `$regenerated` flag)
    38
    39 ### Session Data Storage
    40 Session data is stored as an array accessible through `ArrayObject` interface:
    41 ```php
    42 $session['user_id'] = 123; // Set data
    43 echo $session['user_id']; // Get data
    44 ```
    45
    46 ### Sticky Data
    47 Variables prefixed with `sticky_` are persistent across session resets:
    48 - `sticky__created`: Session creation timestamp
    49 - `sticky__flash`: Flash data lifetime tracking
    50 - `sticky__log`: Regeneration event log
    51 - `sticky__ip`: IP change tracking
    52
    53 ### Internal Tracking Variables
    54 Variables prefixed with `__` are internal session metadata:
    55 - `__started`: Session start time
    56 - `__updated`: Last session update time
    57 - `__regenerated`: Last session ID regeneration time
    58 - `__user_agent`: Client user agent string
    59 - `__user_ip`: Client IP address
    60 - `__user_tls`: TLS/SSL status
    61 - `__nonces`: Active nonce storage
    62 - `__expire`: Session expiration time (for old sessions)
    63
    64 ---
    65
    66 ## Architecture
    67
    68 ### Class Hierarchy
    69 ```
    70 ArrayObject (PHP native)
    71     ↓
    72 Session (abstract)
    73     ↓
    74 [Concrete Implementation] (must implement store_* methods)
    75 ```
    76
    77 ### Key Methods Categories
    78
    79 **Lifecycle Management:**
    80 - `__construct()`: Initialize session object
    81 - `start()`: Begin a session
    82 - `write_close()`: Save and close session
    83 - `restart()`: Destroy and restart session
    84 - `terminator()`: Shutdown handler (garbage collection, flash data cleanup)
    85
    86 **Security:**
    87 - `regenerate_id()`: Replace session ID
    88 - `verify_nonce()`: Validate nonce tokens
    89 - `prevent_replay()`: Anti-replay protection
    90 - `create_nonce()`: Generate nonce tokens
    91
    92 **Storage (Abstract - Must Implement):**
    93 - `store_open()`: Open session storage
    94 - `store_read()`: Read session data
    95 - `store_write()`: Write session data
    96 - `store_close()`: Close session storage
    97 - `store_gc()`: Garbage collection
    98 - `store_validate_id()`: Validate session ID format
    99 - `store_generate_id()`: Generate new session ID
    100
    101 **Cookie Management:**
    102 - `setcookie()`: Set HTTP cookie with security headers
    103 - `get_cookie()`: Retrieve cookie value
    104 - `set_cookie()`: Set cookie (legacy interface)
    105 - `delete_cookie()`: Remove cookie
    106 - `send_cookie()`: Internal cookie transmission
    107
    108 ---
    109
    110 ## Configuration
    111
    112 ### Configuration Properties (Public)
    113
    114 All configuration properties are prefixed with `cf_` (config) and can be set before calling `start()`:
    115
    116 #### Session Behavior
    117 ```php
    118 $session->cf_static = 0; // Disable regenerations (e.g., for CAPTCHA)
    119 $session->cf_max_session = 7200; // Max session lifetime (seconds)
    120 $session->cf_max_idle = 1440; // Max idle time before destruction (seconds)
    121 $session->cf_regen_time = 500; // Seconds between forced ID regenerations
    122 $session->cf_regen_probability = 2; // Percentage probability of forced regen (0-100)
    123 ```
    124
    125 #### Nonce & Replay Protection
    126 ```php
    127 $session->cf_secret = 'adyaiD9+255JeiskPybgisby'; // Secret for nonce generation
    128 $session->cf_nonce_lifetime = 7200; // Nonce expiration (seconds)
    129 $session->cf_prevent_replay = 1; // Enable replay attack prevention
    130 ```
    131
    132 #### Garbage Collection
    133 ```php
    134 $session->cf_gc_probability = 2; // Probability of GC on shutdown (0-100)
    135 $session->cf_gc_maxlifetime = 1440; // Max session file lifetime (seconds)
    136 ```
    137
    138 #### Cookie Settings
    139 ```php
    140 $session->cf_cookie_prefix = ''; // Prefix for all cookies
    141 $session->cf_cookie_persistent = false; // Make cookies persistent
    142 $session->cf_cookie_lifetime = 0; // Cookie lifetime (0 = session cookie)
    143 $session->cf_cookie_path = '/'; // Cookie path
    144 $session->cf_cookie_domain = ''; // Cookie domain ('' = current host)
    145 $session->cf_cookie_secure = false; // HTTPS only
    146 $session->cf_cookie_httponly = true; // Disable JavaScript access
    147 $session->cf_cookie_samesite = COOKIE_SAMESITE; // SameSite attribute
    148 ```
    149
    150 #### Cache Control
    151 ```php
    152 $session->cf_cache_limiter = 'none'; // Cache control mode (public|private|nocache|none)
    153 $session->cf_cache_expire = 180*60; // Cache TTL (seconds)
    154 $session->cf_cache_mtime = 0; // Modify time for Last-Modified header
    155 ```
    156
    157 #### Security Validation
    158 ```php
    159 $session->cf_referer_check = ''; // Check HTTP Referer header
    160 ```
    161
    162 #### HTTP Context (Set by HTTP class)
    163 ```php
    164 $session->cf_ip; // Client IP address
    165 $session->cf_tls; // TLS/SSL connection indicator
    166 ```
    167
    168 ---
    169
    170 ## Usage
    171
    172 ### Basic Session Setup
    173
    174 ```php
    175 // Create a concrete session implementation
    176 class MySession extends Session {
    177     // Implement abstract store_* methods
    178     // See "Implementation Guide" section
    179 }
    180
    181 // Initialize and start session
    182 $session = new MySession();
    183 $session->cf_max_session = 3600; // 1 hour
    184 $session->cf_cookie_path = '/';
    185 $session->start('myapp'); // Session name: 'myapp'
    186
    187 // Store data
    188 $session['user_id'] = 42;
    189 $session['username'] = 'john';
    190
    191 // Retrieve data
    192 echo $session['user_id']; // 42
    193
    194 // Check if session is active
    195 if ($session->active()) {
    196     echo "Session is active";
    197 }
    198
    199 // Explicitly save and close
    200 $session->write_close();
    201
    202 // Shutdown handler automatically called via register_shutdown_function()
    203 ```
    204
    205 ### Session Data Access
    206
    207 ```php
    208 // Array-like access (via ArrayObject)
    209 $session['user_id'] = 123;
    210 echo $session['user_id'];
    211 unset($session['user_id']);
    212 isset($session['user_id']);
    213
    214 // Convert to array
    215 $all_data = $session->toArray();
    216 ```
    217
    218 ### Session ID Management
    219
    220 ```php
    221 // Get current session ID
    222 $id = $session->id(); // Returns: e.g., "abc123xyz..."
    223
    224 // Get session name
    225 $name = $session->name(); // Returns: 'myapp'
    226
    227 // Get session ID from request
    228 $session->start('myapp', $_REQUEST['sid'] ?? null);
    229 ```
    230
    231 ### Session State
    232
    233 ```php
    234 // Check if session is active
    235 if ($session->active()) {
    236     // Session is running
    237 }
    238
    239 // Get last state change message
    240 $message = $session->message(); // 'replay', 'ip', 'ua', 'timeout', etc.
    241
    242 // Restart session (destroy old + start new)
    243 $session->restart();
    244 ```
    245
    246 ---
    247
    248 ## Security Features
    249
    250 ### 1. Session ID Regeneration
    251
    252 **Purpose:** Prevent session fixation attacks
    253
    254 **Automatic Triggers:**
    255 - Initial session creation (`regenerated = 2`)
    256 - First request after creation (`regenerated = 1`)
    257 - Periodic forced regeneration (based on `cf_regen_time` and `cf_regen_probability`)
    258 - Session validation failures
    259
    260 **Manual Trigger:**
    261 ```php
    262 $session->regenerate_id($delete_old = false, $message = 'custom_reason');
    263 ```
    264
    265 **Parameters:**
    266 - `$delete_old`:
    267   - `false` (0): Keep old session active for ~5 seconds (for pending AJAX requests)
    268   - `true` (1): Keep old session for time specified (unused in current code)
    269   - `2`: Immediately destroy old session
    270
    271 **Implementation Details:**
    272 - New session ID is generated via `store_generate_id()`
    273 - Old session data is copied to new ID
    274 - Old session marked with `__expire` timestamp
    275 - Cookie immediately updated with new ID
    276 - Single regeneration per request (checked via `$this->regenerated` flag)
    277 - Logged in `sticky__log` for debugging (max 15 entries)
    278
    279 ```php
    280 // Example: Force regeneration on login
    281 $session->start('myapp');
    282 if ($user_authenticated) {
    283     $session->regenerate_id(false, 'login');
    284     $session['user_id'] = $user->id;
    285 }
    286 ```
    287
    288 ### 2. User Agent Validation
    289
    290 **Purpose:** Detect browser/device changes that might indicate hijacking
    291
    292 **Behavior:**
    293 - Stores user agent on first request
    294 - Compares on subsequent requests using `similar_text()`
    295 - Destroys session if similarity < 95%
    296 - Useful against bot attacks or stolen sessions
    297
    298 **Configuration:**
    299 ```php
    300 // Automatic on each request (if enabled in code logic)
    301 // Triggers session destruction if UA changes significantly
    302 ```
    303
    304 ### 3. IP Address Validation
    305
    306 **Purpose:** Detect IP spoofing or hijacking
    307
    308 **Behavior:**
    309 - Stores IP on first request
    310 - Compares on subsequent requests
    311 - Soft failure on mismatch: `destroy = 1` (keeps regenerating)
    312 - Tracks IP changes in `sticky__ip`
    313
    314 **Configuration:**
    315 ```php
    316 $session->cf_ip = $_SERVER['REMOTE_ADDR']; // Set by HTTP class
    317 // Validation happens automatically during start()
    318 ```
    319
    320 **IP Change Tracking:**
    321 ```php
    322 // Access IP change history
    323 $ip_history = $session->sticky__ip; // Array of [ip => change_count]
    324 ```
    325
    326 ### 4. TLS/SSL Validation
    327
    328 **Purpose:** Prevent protocol downgrade attacks
    329
    330 **Behavior:**
    331 - Checks if connection transitioned from HTTPS to HTTP
    332 - Destroys session on mismatch
    333
    334 **Configuration:**
    335 ```php
    336 $session->cf_tls = !empty($_SERVER['HTTPS']); // Set by HTTP class
    337 // Validation happens automatically during start()
    338 ```
    339
    340 ### 5. Anti-Replay Protection
    341
    342 **Purpose:** Prevent CSRF and replay attacks
    343
    344 **Mechanism:**
    345 - Generates unique "NoReplay" nonce on each request
    346 - Cookie-based nonce verification
    347 - Detects rapid-fire requests (AJAX attacks)
    348
    349 **Configuration:**
    350 ```php
    351 $session->cf_prevent_replay = 1; // Enable (default)
    352 $session->cf_prevent_replay = 0; // Disable if needed
    353 ```
    354
    355 **How It Works:**
    356 ```
    357 Request 1: Generate nonce, send in cookie
    358 Request 2: Client sends nonce back, verify & generate new one
    359 Request 3: If old nonce used again → reject (replay detected)
    360 ```
    361
    362 ### 6. Referer Validation (Optional)
    363
    364 **Purpose:** Prevent CSRF via header checking
    365
    366 **Configuration:**
    367 ```php
    368 $session->cf_referer_check = 'example.com';
    369 // Session rejected if HTTP_REFERER doesn't contain this string
    370 ```
    371
    372 ---
    373
    374 ## API Reference
    375
    376 ### Public Methods
    377
    378 #### Lifecycle Management
    379
    380 ##### `start($name = null, $id = null): bool`
    381 Start or resume a session.
    382
    383 **Parameters:**
    384 - `$name` (string|null): Session name (cookie name base). Alphanumeric + underscore/dash. Defaults to 'sesid'
    385 - `$id` (string|null): Existing session ID to resume. If null, attempts to read from cookie
    386
    387 **Returns:** `true` if session started successfully, `false` on error
    388
    389 **Side Effects:**
    390 - Sets headers (cookies, cache control)
    391 - Populates session data from storage
    392 - Performs security validations
    393 - May trigger session ID regeneration
    394
    395 **Example:**
    396 ```php
    397 if ($session->start('webapp', $_COOKIE['sess_id'] ?? null)) {
    398     // Session ready
    399 } else {
    400     // Session failed
    401 }
    402 ```
    403
    404 **Validation Steps:**
    405 1. Reject if headers already sent
    406 2. Validate session name format
    407 3. Retrieve ID from parameter or cookie
    408 4. Check Referer header (if configured)
    409 5. Validate ID format via `store_validate_id()`
    410 6. Read session data from storage
    411 7. Verify nonces and timestamps
    412 8. Check user agent, IP, TLS
    413 9. Regenerate if needed
    414
    415 ---
    416
    417 ##### `write_close(): void`
    418 Save session data and close session.
    419
    420 **Side Effects:**
    421 - Calls `write_session()` to serialize and store data
    422 - Calls `store_close()` to close storage handler
    423 - Sets `$active = false`
    424
    425 **Example:**
    426 ```php
    427 $session['key'] = 'value';
    428 $session->write_close(); // Ensure data is saved
    429 ```
    430
    431 ---
    432
    433 ##### `restart(): bool`
    434 Destroy current session and create new one.
    435
    436 **Equivalent to:** `regenerate_id(true) + clean_vars() + populate()`
    437
    438 **Returns:** `true` on success, `false` on error
    439
    440 **Use Cases:**
    441 - User logout and new login
    442 - Security reset
    443 - Complete session refresh
    444
    445 **Example:**
    446 ```php
    447 $session->restart();
    448 // New session created, old data cleared, sticky_ vars preserved
    449 ```
    450
    451 ---
    452
    453 #### Session Access
    454
    455 ##### `id(): mixed`
    456 Get current session ID.
    457
    458 **Returns:** Session ID string or null if not started
    459
    460 ```php
    461 $sid = $session->id(); // "abc123xyz..."
    462 ```
    463
    464 ---
    465
    466 ##### `name(): string`
    467 Get session name (cookie prefix).
    468
    469 **Returns:** Session name
    470
    471 ```php
    472 $name = $session->name(); // "myapp"
    473 ```
    474
    475 ---
    476
    477 ##### `active(): bool`
    478 Check if session is currently active.
    479
    480 **Returns:** `true` if session is started and active, `false` otherwise
    481
    482 ```php
    483 if ($session->active()) {
    484     $session['key'] = 'value';
    485 }
    486 ```
    487
    488 ---
    489
    490 ##### `message(): string|null`
    491 Get reason for last session state change.
    492
    493 **Returns:** Message string or null
    494
    495 **Possible Values:**
    496 - `'replay'`: Replay attack detected
    497 - `'obsolete'`: Session marked for expiration
    498 - `'reg_expire'`: Regeneration expiration reached
    499 - `'max_session'`: Max session lifetime exceeded
    500 - `'max_idle'`: Idle timeout exceeded
    501 - `'ua'`: User agent mismatch (>5% difference)
    502 - `'tls'`: TLS status changed
    503 - `'ip'`: IP address mismatch
    504 - `'restart'`: Session manually restarted
    505 - `null`: No state change
    506
    507 **Example:**
    508 ```php
    509 $session->start('app');
    510 if ($message = $session->message()) {
    511     error_log("Session issue: $message");
    512 }
    513 ```
    514
    515 ---
    516
    517 ##### `toArray(): array`
    518 Convert session data to array.
    519
    520 **Returns:** Associative array of session data
    521
    522 **Note:** This is a direct call to `ArrayObject::getArrayCopy()`
    523
    524 ```php
    525 $data = $session->toArray();
    526 foreach ($data as $key => $value) {
    527     echo "$key => $value\n";
    528 }
    529 ```
    530
    531 ---
    532
    533 #### Nonce System
    534
    535 ##### `create_nonce($action, $expires = null): string`
    536 Generate a unique nonce token.
    537
    538 **Parameters:**
    539 - `$action` (string): Action identifier (e.g., 'form_submit', 'delete_action')
    540 - `$expires` (int|null): Expiration time in seconds. Defaults to `cf_nonce_lifetime`
    541
    542 **Returns:** Nonce token string (11 characters)
    543
    544 **Example:**
    545 ```php
    546 $nonce = $session->create_nonce('form_submit', 3600);
    547 // Use in HTML: <input type="hidden" name="nonce" value="<?= $nonce ?>">
    548 ```
    549
    550 **Storage:**
    551 - Stored in `$session->__nonces[]`
    552 - Key: `{action}.{base64_encoded_hash}`
    553 - Value: Expiration timestamp
    554
    555 ---
    556
    557 ##### `verify_nonce($action, $code, $protect = 0)`
    558 Verify a nonce token.
    559
    560 **Parameters:**
    561 - `$action` (string): Action identifier that was used in `create_nonce()`
    562 - `$code` (string): Nonce token from user
    563 - `$protect` (int): Protection level
    564   - `0`: Single-use nonce (consumed on first verification)
    565   - `1+`: Protected nonce (can verify multiple times, prevents fast replays)
    566
    567 **Returns:**
    568 - `true` (1): Nonce verified and valid
    569 - `false` (0): Nonce invalid or expired
    570 - `-1`: Protected nonce used twice in quick succession (possible AJAX attack)
    571
    572 **Example:**
    573 ```php
    574 if ($nonce = $session->verify_nonce('form_submit', $_POST['nonce'])) {
    575     if ($nonce === -1) {
    576         // Possible replay, but might be legitimate AJAX
    577         $session->cf_prevent_replay = 0; // Disable for this request
    578     } else {
    579         // Safe to process
    580         process_form();
    581     }
    582 }
    583 ```
    584
    585 **Cleanup:**
    586 - Expired nonces automatically removed
    587 - Verified single-use nonces removed from storage
    588
    589 ---
    590
    591 #### Cookie Management
    592
    593 ##### `setcookie($name, $value = null, $expires = 0, $path = null, $domain = null, $secure = null, $httponly = null, $samesite = null): bool`
    594 Set a cookie with security headers.
    595
    596 **Parameters:**
    597 - `$name`: Cookie name (automatically URL-encoded)
    598 - `$value`: Cookie value (automatically URL-encoded, null to delete)
    599 - `$expires`: Expiration timestamp (0 = session cookie)
    600 - `$path`: Cookie path (default: `cf_cookie_path`)
    601 - `$domain`: Cookie domain (default: `cf_cookie_domain`)
    602 - `$secure`: HTTPS only (default: `cf_cookie_secure`)
    603 - `$httponly`: Disable JS access (default: `cf_cookie_httponly`)
    604 - `$samesite`: SameSite attribute (default: `cf_cookie_samesite`)
    605
    606 **Returns:** `true` on success, `false` if headers already sent
    607
    608 **Features:**
    609 - RFC 2616 2.2 token encoding for cookie name
    610 - RFC 6265 4.1.1 cookie-octet encoding for value
    611 - Removes duplicate cookie headers automatically
    612 - Adds all security attributes (secure, httponly, samesite)
    613 - Does NOT replace existing cookies (allows multiple Set-Cookie headers)
    614
    615 **Example:**
    616 ```php
    617 // Session cookie
    618 $session->setcookie('user_pref', 'dark_mode');
    619
    620 // Persistent cookie (30 days)
    621 $session->setcookie('remember_me', 'token123', time() + 30*86400);
    622
    623 // Delete cookie
    624 $session->setcookie('old_cookie', null);
    625
    626 // Secure cookie with SameSite
    627 $session->setcookie('token', 'abc123', time() + 3600,
    628     path: '/', secure: true, httponly: true, samesite: 'Strict');
    629 ```
    630
    631 ---
    632
    633 ##### `get_cookie($name)`
    634 Retrieve cookie value.
    635
    636 **Parameters:**
    637 - `$name`: Cookie name (prefix automatically added)
    638
    639 **Returns:** Cookie value or null if not set
    640
    641 ```php
    642 $value = $session->get_cookie('user_pref'); // Reads $_COOKIE['user_pref']
    643 ```
    644
    645 ---
    646
    647 ##### `set_cookie($name, $value, $persistent = false): void`
    648 Legacy cookie setter (alternative to `setcookie()`).
    649
    650 **Parameters:**
    651 - `$name`: Cookie name (prefix added)
    652 - `$value`: Cookie value
    653 - `$persistent`:
    654   - `false`: Session cookie (deleted on browser close)
    655   - Number: Days to persist
    656   - `0`: Use `cf_cookie_persistent` config
    657
    658 **Example:**
    659 ```php
    660 $session->set_cookie('theme', 'dark'); // Session cookie
    661 $session->set_cookie('lang', 'en', 365); // 1 year
    662 ```
    663
    664 ---
    665
    666 ##### `delete_cookie($name): void`
    667 Delete a cookie.
    668
    669 **Parameters:**
    670 - `$name`: Cookie name (prefix added)
    671
    672 **Implementation:** Sets empty value with immediate expiration
    673
    674 ```php
    675 $session->delete_cookie('old_preference');
    676 ```
    677
    678 ---
    679
    680 ##### `unsetcookie($name): void`
    681 Alias for `setcookie($name)` with no value (convenience method).
    682
    683 ```php
    684 $session->unsetcookie('cookie_name');
    685 ```
    686
    687 ---
    688
    689 ### Protected Methods (For Store Implementation)
    690
    691 #### `regenerate_id($delete_old = false, $message = ''): bool`
    692 Internal method to regenerate session ID (called automatically).
    693
    694 **Protected** - Usually called automatically, but can be overridden/called by subclasses
    695
    696 ---
    697
    698 #### `store_generate_id(): string`
    699 Generate a new session ID.
    700
    701 **Default Implementation:** Returns 21-character random alphanumeric string via `Ut::random_token(21)`
    702
    703 **Override in subclass to customize:**
    704 ```php
    705 protected function store_generate_id(): string {
    706     return hash('sha256', random_bytes(32)); // Your format
    707 }
    708 ```
    709
    710 ---
    711
    712 #### `store_validate_id($id): bool`
    713 Validate session ID format.
    714
    715 **Default Implementation:** Regex check: `/^[a-zA-Z\d]{21}$/`
    716
    717 **Override in subclass to match your format:**
    718 ```php
    719 protected function store_validate_id($id): bool {
    720     return preg_match('/^[a-f0-9]{64}$/', $id); // SHA256 format
    721 }
    722 ```
    723
    724 ---
    725
    726 #### `store_open($name): void`
    727 Open session storage (called before first read/write).
    728
    729 **Subclass must implement** - Initialize storage handler
    730
    731 **Example:**
    732 ```php
    733 protected function store_open($name): void {
    734     $this->db = new PDO('sqlite::memory:');
    735 }
    736 ```
    737
    738 ---
    739
    740 #### `store_read($id, $lock = false): string|false`
    741 Read session data from storage.
    742
    743 **Subclass must implement**
    744
    745 **Parameters:**
    746 - `$id`: Session ID to read
    747 - `$lock`: If true, lock the session file for writing (create new)
    748
    749 **Returns:**
    750 - Serialized session data (string) if found and locked
    751 - Empty string (`''`) if new session should be created
    752 - `false` if session doesn't exist or read error
    753
    754 **Example:**
    755 ```php
    756 protected function store_read($id, $lock = false): string|false {
    757     $data = file_get_contents("/tmp/sess_$id");
    758     return $data ?: false;
    759 }
    760 ```
    761
    762 ---
    763
    764 #### `store_write($id, $data): void`
    765 Write session data to storage.
    766
    767 **Subclass must implement**
    768
    769 **Parameters:**
    770 - `$id`: Session ID
    771 - `$data`: Serialized session data (already processed by `Ut::serialize()`)
    772
    773 **Example:**
    774 ```php
    775 protected function store_write($id, $data): void {
    776     file_put_contents("/tmp/sess_$id", $data);
    777 }
    778 ```
    779
    780 ---
    781
    782 #### `store_close(): void`
    783 Close session storage.
    784
    785 **Subclass must implement** - Release resources
    786
    787 **Example:**
    788 ```php
    789 protected function store_close(): void {
    790     // Close database, file, etc.
    791 }
    792 ```
    793
    794 ---
    795
    796 #### `store_gc(): void`
    797 Perform garbage collection on old sessions.
    798
    799 **Subclass must implement** - Delete expired sessions
    800
    801 **Called During:**
    802 - Shutdown handler (probabilistic, based on `cf_gc_probability`)
    803
    804 **Should Delete:**
    805 - Sessions older than `cf_gc_maxlifetime` seconds
    806
    807 **Example:**
    808 ```php
    809 protected function store_gc(): void {
    810     $max_age = time() - $this->cf_gc_maxlifetime;
    811     // Delete files/records older than $max_age
    812 }
    813 ```
    814
    815 ---
    816
    817 ### Private Methods (Internal Use)
    818
    819 ##### `populate(): void`
    820 Initialize session tracking variables on first request.
    821
    822 **Called by:** `start()`, `restart()`
    823
    824 **Initializes:**
    825 - `__started`: Current timestamp
    826 - `__regenerated`: Current timestamp
    827 - `__user_agent`: Browser user agent
    828 - `__user_ip`: Client IP (if configured)
    829 - `__user_tls`: TLS status (if configured)
    830 - `sticky__created`: Creation time (if not exists)
    831
    832 ---
    833
    834 ##### `write_session(): void`
    835 Serialize and write session data to storage.
    836
    837 **Called by:** `regenerate_id()`, `write_close()`, `terminator()`
    838
    839 **Updates:**
    840 - `__updated`: Current timestamp
    841 - Calls `store_write()` with serialized data
    842
    843 ---
    844
    845 ##### `clean_vars(): void`
    846 Remove non-sticky session variables.
    847
    848 **Called by:** `restart()`, session validation failure
    849
    850 **Preserves:** Variables starting with `sticky_`
    851
    852 ---
    853
    854 ##### `prevent_replay(): void`
    855 Generate and send anti-replay nonce.
    856
    857 **Called by:** `populate()`
    858
    859 **Action:**
    860 - Creates 'NoReplay' nonce
    861 - Sends in cookie: `{cf_cookie_prefix}NoReplay`
    862
    863 ---
    864
    865 ##### `cache_limiter(): void`
    866 Set HTTP cache control headers based on configuration.
    867
    868 **Called by:** `start()` after session data loaded
    869
    870 **Modes:**
    871 - `'public'`: Cacheable, `Cache-Control: public, max-age=...`
    872 - `'private'`: Private, `Cache-Control: private, max-age=...`
    873 - `'private_no_expire'`: Private no TTL
    874 - `'nocache'`: No storage, `Cache-Control: no-store`
    875 - `'none'`: No headers (default)
    876
    877 ---
    878
    879 ##### `set_new_id(): void`
    880 Generate and assign new session ID, send in cookie.
    881
    882 **Called by:** `regenerate_id()`, `start()` (for new sessions)
    883
    884 ---
    885
    886 ##### `remove_cookie($cookie): void`
    887 Remove existing Set-Cookie header to avoid duplicates.
    888
    889 **Called by:** `setcookie()` before setting new value
    890
    891 ---
    892
    893 ##### `nonce_index($action, $code): string` (static)
    894 Generate storage key for nonce.
    895
    896 **Returns:** `{action}.{base64_encoded_hash}`
    897
    898 ---
    899
    900 ---
    901
    902 ## Session Lifecycle
    903
    904 ### Complete Session Flow
    905
    906 ```
    907 ┌─ Browser Request
    908
    909 ├─ Application Code
    910 │ └─ $session->start('appname')
    911 │ │
    912 │ ├─ Check if headers sent
    913 │ ├─ Validate/read session name
    914 │ ├─ Get session ID from:
    915 │ │ 1. Parameter $id
    916 │ │ 2. Cookie: {prefix}appname
    917 │ ├─ Validate referer (if cf_referer_check set)
    918 │ ├─ Validate ID format via store_validate_id()
    919 │ ├─ store_open(name)
    920 │ ├─ store_read(id)
    921 │ │ └─ If missing/invalid/expired:
    922 │ │ └─ set_new_id()
    923 │ │ └─ regenerate_id = 2 (NEW)
    924 │ ├─ Deserialize session data
    925 │ ├─ exchangeArray(data)
    926 │ ├─ active = true
    927 │ ├─ cache_limiter()
    928 │ │
    929 │ └─ Security Checks (if NOT first request):
    930 │ ├─ Verify NoReplay nonce
    931 │ ├─ Check expiration flags
    932 │ ├─ Check max session time
    933 │ ├─ Check max idle time
    934 │ ├─ Compare user agent (95%+ similarity)
    935 │ ├─ Compare TLS status
    936 │ ├─ Compare IP address
    937 │ │ ├─ Match: OK
    938 │ │ └─ Mismatch: destroy=1, regenerate
    939 │ └─ Check regen time/probability
    940 │ └─ regenerate_id()
    941
    942 ├─ Application Code
    943 │ └─ $session['key'] = 'value'
    944
    945 └─ End of Request
    946    │
    947    └─ register_shutdown_function() → terminator()
    948       │
    949       ├─ Process flash data
    950       │ └─ Decrement lifetimes
    951       │ └─ Remove expired flash
    952       ├─ write_session()
    953       │ └─ store_write(id, serialized_data)
    954       ├─ store_close()
    955       ├─ Probabilistic garbage collection
    956       │ └─ store_gc() (cf_gc_probability % chance)
    957       │ └─ Delete old sessions
    958       └─ Output sent to browser
    959 ```
    960
    961 ### First Request (New Session)
    962
    963 ```
    964 start() is called
    965 ├─ No ID in cookie
    966 ├─ store_read(id) → false
    967 ├─ set_new_id()
    968 │ └─ id = store_generate_id()
    969 │ └─ send_cookie(name, id)
    970 ├─ data = []
    971 ├─ active = true
    972 ├─ populate()
    973 │ ├─ __started = now
    974 │ ├─ __regenerated = now
    975 │ ├─ __user_agent = UA
    976 │ └─ sticky__created = now
    977 └─ return true
    978 ```
    979
    980 ### Subsequent Request (Resume Session)
    981
    982 ```
    983 start() is called
    984 ├─ ID from cookie
    985 ├─ store_read(id) → serialized_data
    986 ├─ data = unserialize(data)
    987 ├─ exchangeArray(data)
    988 ├─ active = true
    989 ├─ Security checks:
    990 │ ├─ Replay check
    991 │ ├─ Timeout checks
    992 │ ├─ UA/IP/TLS checks
    993 │ └─ May trigger regenerate_id()
    994 └─ return true
    995 ```
    996
    997 ### Session ID Regeneration
    998
    999 ```
    1000 regenerate_id($delete_old, $message) is called
    1001 ├─ Check not headers_sent()
    1002 ├─ Check $active
    1003 ├─ Check not already regenerated in this request
    1004 ├─ write_session() [Save current data]
    1005 ├─ set __expire:
    1006 │ ├─ if $delete_old=0: __expire = now + 5
    1007 │ └─ if $delete_old>0: __expire = 0
    1008 ├─ Generate new ID:
    1009 │ └─ loop:
    1010 │ ├─ id = store_generate_id()
    1011 │ └─ while store_read(id) !== false [Ensure unique]
    1012 ├─ Lock new session: store_read(id, true)
    1013 ├─ Set: __regenerated = now
    1014 ├─ Set: regenerated = 1
    1015 ├─ Log event: sticky__log[] = [now, message]
    1016 └─ return true
    1017 ```
    1018
    1019 ### Session Destruction
    1020
    1021 ```
    1022 Triggered by:
    1023 ├─ restart() → regenerate_id(true)
    1024 ├─ Validation failure (destroy=2)
    1025 │ └─ regenerate_id(2)
    1026 │ └─ clean_vars() [Remove non-sticky data]
    1027 └─ Timeout or security violation
    1028 Results in:
    1029 ├─ __expire = 0 [Immediate expiration]
    1030 ├─ Non-sticky variables cleared
    1031 ├─ sticky_ variables preserved
    1032 └─ New session ID generated
    1033 ```
    1034
    1035 ---
    1036
    1037 ## Flash Data
    1038
    1039 Flash data persists for a limited number of requests (typically 1-2) and is automatically removed.
    1040
    1041 ### Usage
    1042
    1043 ```php
    1044 // Store flash message for next request
    1045 $session->set_flash('error', 'Username already exists', 1); // 1 request
    1046 $session->set_flash('info', 'Welcome back!', 2); // 2 requests
    1047
    1048 // In next request, data automatically available
    1049 echo $session['error']; // "Username already exists"
    1050 ```
    1051
    1052 ### How It Works
    1053
    1054 1. **Storage:** Flash data stored in `$session->sticky__flash`
    1055    - Key: Variable name
    1056    - Value: Lifetime in requests
    1057
    1058 2. **Cleanup:** In `terminator()` (shutdown handler):
    1059    ```php
    1060    foreach ($sticky__flash as $var => $age) {
    1061        if (!isset($session[$var])) {
    1062            unset($sticky__flash[$var]); // Already deleted
    1063        } else if (--$age <= 0) {
    1064            unset($session[$var]); // Expired, remove
    1065            unset($flash__flash[$var]);
    1066        } else {
    1067            $flash__flash[$var] = $age; // Decrement counter
    1068        }
    1069    }
    1070    ```
    1071
    1072 3. **Persistence:** Flash variables are kept in `sticky__flash` even during session resets
    1073
    1074 ### Example: Login Flow
    1075
    1076 ```php
    1077 // POST /login
    1078 if ($credentials_valid) {
    1079     $session->restart(); // New session
    1080     $session['user_id'] = $user->id;
    1081     $session->set_flash('success', 'Login successful!', 1);
    1082     header('Location: /dashboard');
    1083 } else {
    1084     $session->set_flash('error', 'Invalid credentials', 1);
    1085     header('Location: /login');
    1086 }
    1087
    1088 // GET /dashboard (or /login on failure)
    1089 if ($message = $session['error'] ?? null) {
    1090     echo "<div class='error'>$message</div>";
    1091 }
    1092 if ($message = $session['success'] ?? null) {
    1093     echo "<div class='success'>$message</div>";
    1094 }
    1095 ```
    1096
    1097 ---
    1098
    1099 ## Nonce System
    1100
    1101 Nonces provide CSRF protection and replay attack detection.
    1102
    1103 ### Terminology
    1104
    1105 - **Nonce:** Number used ONCE - cryptographic token for action verification
    1106 - **Action:** Type of operation being protected (e.g., 'form_submit', 'delete_user')
    1107 - **Protected Nonce:** Can be verified multiple times with protection against rapid reuse
    1108
    1109 ### Complete Example: Form Protection
    1110
    1111 ```php
    1112 // 1. Display form with nonce
    1113 $nonce = $session->create_nonce('user_update', 3600);
    1114 ?>
    1115 <form method="POST" action="/update-profile">
    1116     <input type="hidden" name="nonce" value="<?= htmlspecialchars($nonce) ?>">
    1117     <input type="text" name="username" value="...">
    1118     <button type="submit">Update</button>
    1119 </form>
    1120
    1121 <?php
    1122 // 2. Process form submission
    1123 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    1124     if (!$session->verify_nonce('user_update', $_POST['nonce'] ?? '')) {
    1125         http_response_code(403);
    1126         die('Security check failed');
    1127     }
    1128
    1129     // Safe to process
    1130     update_user($_POST);
    1131 }
    1132 ```
    1133
    1134 ### Example: Protected Nonce (AJAX-Safe)
    1135
    1136 ```php
    1137 // Generate protected nonce (can verify multiple times)
    1138 $nonce = $session->create_nonce('ajax_action', 300);
    1139
    1140 // Verify with protection level 3 (3 seconds)
    1141 $result = $session->verify_nonce('ajax_action', $_POST['nonce'], 3);
    1142
    1143 if ($result === -1) {
    1144     // Rapid reuse detected (possible attack, but might be AJAX)
    1145     if (is_ajax_request()) {
    1146         // AJAX is OK, disable replay protection this once
    1147         $session->cf_prevent_replay = 0;
    1148     } else {
    1149         // Likely attack
    1150         http_response_code(403);
    1151         die('Suspicious activity');
    1152     }
    1153 } else if ($result === true) {
    1154     // Safe to process
    1155     process_ajax();
    1156 }
    1157 ```
    1158
    1159 ### Nonce Storage Format
    1160
    1161 ```
    1162 Internal storage (__nonces array):
    1163 [
    1164     "{action}.{hash}" => expiration_timestamp,
    1165     "form_submit.AbCdEfGhIjK" => 1234567890,
    1166     "delete_user.XyZaBcDeFgH" => 1234567890,
    1167 ]
    1168 Where:
    1169 - action: Custom action identifier
    1170 - hash: First 11 chars of base64(sha1(code_bytes))
    1171 - expiration_timestamp: time() + lifetime
    1172 ```
    1173
    1174 ### Security Properties
    1175
    1176 - **CSRF Protection:** Nonce must match to process form
    1177 - **One-Time Use:** Each nonce consumed after first verification (unless protected)
    1178 - **Expiration:** Nonces automatically expire
    1179 - **Action-Specific:** Each action has separate nonce space
    1180 - **AJAX-Safe:** Protected nonces allow multiple quick verifications
    1181
    1182 ---
    1183
    1184 ## Cookie Management
    1185
    1186 ### Security Features
    1187
    1188 The `setcookie()` method implements comprehensive cookie security:
    1189
    1190 #### Encoding
    1191 ```php
    1192 // Cookie names: RFC 2616 2.2 token format
    1193 // Cookie values: RFC 6265 4.1.1 cookie-octet format
    1194 // Unsafe characters automatically URL-encoded
    1195 ```
    1196
    1197 #### Security Attributes
    1198 ```php
    1199 setcookie('auth', 'token',
    1200     expires: time() + 3600,
    1201     secure: true, // HTTPS only
    1202     httponly: true, // Disable JavaScript
    1203     samesite: 'Strict' // CSRF protection
    1204 );
    1205 ```
    1206
    1207 #### No Duplicate Headers
    1208 ```php
    1209 // Automatically removes old Set-Cookie header before setting new one
    1210 // Prevents cookie header duplication
    1211 remove_cookie($name) → clears old headers
    1212 setcookie() → sets new header
    1213 ```
    1214
    1215 ### Configuration-Driven Defaults
    1216
    1217 ```php
    1218 $session->cf_cookie_path = '/app'; // Path
    1219 $session->cf_cookie_domain = '.example.com'; // Domain
    1220 $session->cf_cookie_secure = true; // HTTPS
    1221 $session->cf_cookie_httponly = true; // No JS
    1222 $session->cf_cookie_samesite = 'Lax'; // SameSite
    1223 $session->cf_cookie_prefix = 'app_'; // Prefix
    1224
    1225 $session->setcookie('token', 'value');
    1226 // Uses all configured defaults
    1227 ```
    1228
    1229 ### Typical Secure Configuration
    1230
    1231 ```php
    1232 // Prevent XSS and CSRF
    1233 $session->cf_cookie_secure = true; // HTTPS only
    1234 $session->cf_cookie_httponly = true; // No JavaScript access
    1235 $session->cf_cookie_samesite = 'Strict'; // Strict CSRF protection
    1236
    1237 // Set scope
    1238 $session->cf_cookie_path = '/'; // Root path
    1239 $session->cf_cookie_domain = ''; // Current host only
    1240
    1241 // Session cookies (delete on browser close)
    1242 $session->cf_cookie_lifetime = 0;
    1243 $session->cf_cookie_persistent = false;
    1244 ```
    1245
    1246 ---
    1247
    1248 ## Error Handling
    1249
    1250 ### Graceful Degradation
    1251
    1252 The Session class gracefully handles errors:
    1253
    1254 #### Headers Already Sent
    1255 ```php
    1256 if (headers_sent($file, $line)) {
    1257     trigger_error("id regeneration requested after headers flushed at $file:$line",
    1258                   E_USER_WARNING);
    1259     return false;
    1260 }
    1261 ```
    1262
    1263 **Impact:** Session ID cannot be regenerated, but session continues
    1264
    1265 #### Cookie Setting Failure
    1266 ```php
    1267 if (headers_sent($file, $line)) {
    1268     trigger_error("cannot place session cookie $name=$value due to $file:$line",
    1269                   E_USER_WARNING);
    1270     return;
    1271 }
    1272 ```
    1273
    1274 **Impact:** Cookie not set, but session data remains accessible
    1275
    1276 #### Storage Errors
    1277 ```php
    1278 if ($this->store_read($this->id, true) !== '') {
    1279     // error! [comment indicates error, but continues]
    1280 }
    1281 ```
    1282
    1283 **Impact:** Creates new session if storage returns error
    1284
    1285 ### Debug Logging
    1286
    1287 The Session class includes commented debug statements:
    1288
    1289 ```php
    1290 # Ut::dbg("regeneration failed by flush at $file:$line");
    1291 # Ut::dbg($destroy, $message);
    1292 # Ut::dbg("session setcookie $name failed by $file:$line");
    1293 ```
    1294
    1295 To enable: Uncomment lines and ensure `Ut::dbg()` function exists
    1296
    1297 ### Event Logging
    1298
    1299 Session events tracked in `sticky__log`:
    1300
    1301 ```php
    1302 // Access session event history
    1303 if (isset($session->sticky__log)) {
    1304     foreach ($session->sticky__log as [$timestamp, $message]) {
    1305         echo "[$timestamp] $message\n";
    1306     }
    1307 }
    1308 ```
    1309
    1310 **Logged Events:**
    1311 - Session regeneration (with reason)
    1312 - Limited to 15 most recent events (old entries archived as '...')
    1313
    1314 ---
    1315
    1316 ## Implementation Guide
    1317
    1318 ### Creating a Concrete Session Class
    1319
    1320 You must implement the abstract storage methods. Choose your storage backend: files, database, cache, etc.
    1321
    1322 #### File-Based Storage
    1323
    1324 ```php
    1325 <?php
    1326
    1327 class FileSession extends Session {
    1328     private $session_dir = '/tmp/sessions';
    1329     private $file_handle = null;
    1330
    1331     public function __construct() {
    1332         parent::__construct();
    1333         if (!is_dir($this->session_dir)) {
    1334             mkdir($this->session_dir, 0700, true);
    1335         }
    1336     }
    1337
    1338     protected function store_open($name): void {
    1339         // PHP sessions don't really "open", just prepare
    1340         // In file mode, we could initialize directory
    1341     }
    1342
    1343     protected function store_read($id, $lock = false): string|false {
    1344         $file = $this->session_dir . '/sess_' . preg_replace('/[^a-zA-Z0-9]/', '', $id);
    1345
    1346         if (!file_exists($file)) {
    1347             if ($lock) {
    1348                 // Create new session file
    1349                 file_put_contents($file, '', LOCK_EX);
    1350                 return '';
    1351             }
    1352             return false;
    1353         }
    1354
    1355         if (filemtime($file) < time() - $this->cf_gc_maxlifetime) {
    1356             unlink($file); // Expired
    1357             return false;
    1358         }
    1359
    1360         return file_get_contents($file);
    1361     }
    1362
    1363     protected function store_write($id, $data): void {
    1364         $file = $this->session_dir . '/sess_' . preg_replace('/[^a-zA-Z0-9]/', '', $id);
    1365         file_put_contents($file, $data, LOCK_EX);
    1366     }
    1367
    1368     protected function store_close(): void {
    1369         // No cleanup needed for file backend
    1370     }
    1371
    1372     protected function store_gc(): void {
    1373         $cutoff = time() - $this->cf_gc_maxlifetime;
    1374         foreach (glob($this->session_dir . '/sess_*') as $file) {
    1375             if (filemtime($file) < $cutoff) {
    1376                 unlink($file);
    1377             }
    1378         }
    1379     }
    1380 }
    1381 ```
    1382
    1383 #### Database Storage (PDO)
    1384
    1385 ```php
    1386 <?php
    1387
    1388 class DatabaseSession extends Session {
    1389     private PDO $pdo;
    1390
    1391     public function __construct(PDO $pdo) {
    1392         parent::__construct();
    1393         $this->pdo = $pdo;
    1394         $this->ensure_table();
    1395     }
    1396
    1397     private function ensure_table(): void {
    1398         $sql = <<<SQL
    1399             CREATE TABLE IF NOT EXISTS sessions (
    1400                 id VARCHAR(21) PRIMARY KEY,
    1401                 data LONGTEXT NOT NULL,
    1402                 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    1403                 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
    1404             )
    1405         SQL;
    1406         $this->pdo->exec($sql);
    1407     }
    1408
    1409     protected function store_open($name): void {
    1410         // Database already connected
    1411     }
    1412
    1413     protected function store_read($id, $lock = false): string|false {
    1414         $stmt = $this->pdo->prepare('SELECT data FROM sessions WHERE id = ?');
    1415         $stmt->execute([$id]);
    1416
    1417         if ($result = $stmt->fetch(PDO::FETCH_ASSOC)) {
    1418             return $result['data'];
    1419         }
    1420
    1421         if ($lock) {
    1422             // Create new session
    1423             $stmt = $this->pdo->prepare('INSERT INTO sessions (id, data) VALUES (?, ?)');
    1424             $stmt->execute([$id, '']);
    1425             return '';
    1426         }
    1427
    1428         return false;
    1429     }
    1430
    1431     protected function store_write($id, $data): void {
    1432         $stmt = $this->pdo->prepare(
    1433             'INSERT INTO sessions (id, data) VALUES (?, ?)
    1434              ON DUPLICATE KEY UPDATE data = VALUES(data)'
    1435         );
    1436         $stmt->execute([$id, $data]);
    1437     }
    1438
    1439     protected function store_close(): void {
    1440         // Connection persists for application
    1441     }
    1442
    1443     protected function store_gc(): void {
    1444         $cutoff = time() - $this->cf_gc_maxlifetime;
    1445         $this->pdo->prepare('DELETE FROM sessions WHERE updated_at < FROM_UNIXTIME(?)')
    1446                    ->execute([$cutoff]);
    1447     }
    1448 }
    1449 ```
    1450
    1451 #### Redis Storage
    1452
    1453 ```php
    1454 <?php
    1455
    1456 class RedisSession extends Session {
    1457     private Redis $redis;
    1458     private string $prefix = 'sess:';
    1459
    1460     public function __construct(Redis $redis) {
    1461         parent::__construct();
    1462         $this->redis = $redis;
    1463     }
    1464
    1465     protected function store_open($name): void {
    1466         // Redis already connected
    1467     }
    1468
    1469     protected function store_read($id, $lock = false): string|false {
    1470         $data = $this->redis->get($this->prefix . $id);
    1471
    1472         if ($data !== false) {
    1473             return $data;
    1474         }
    1475
    1476         if ($lock) {
    1477             // Create new session
    1478             $this->redis->set($this->prefix . $id, '',
    1479                             ['EX' => $this->cf_gc_maxlifetime]);
    1480             return '';
    1481         }
    1482
    1483         return false;
    1484     }
    1485
    1486     protected function store_write($id, $data): void {
    1487         $this->redis->set($this->prefix . $id, $data,
    1488                         ['EX' => $this->cf_gc_maxlifetime]);
    1489     }
    1490
    1491     protected function store_close(): void {
    1492         // Connection persists
    1493     }
    1494
    1495     protected function store_gc(): void {
    1496         // Redis handles expiration automatically with TTL
    1497     }
    1498 }
    1499 ```
    1500
    1501 ### Complete Integration Example
    1502
    1503 ```php
    1504 <?php
    1505
    1506 // Initialize session with configuration
    1507 $session = new FileSession();
    1508
    1509 // Configure security
    1510 $session->cf_cookie_secure = (!empty($_SERVER['HTTPS']));
    1511 $session->cf_cookie_httponly = true;
    1512 $session->cf_cookie_samesite = 'Lax';
    1513 $session->cf_max_session = 86400; // 24 hours
    1514 $session->cf_max_idle = 3600; // 1 hour
    1515 $session->cf_prevent_replay = true;
    1516
    1517 // Set IP and TLS validation
    1518 $session->cf_ip = $_SERVER['REMOTE_ADDR'];
    1519 $session->cf_tls = !empty($_SERVER['HTTPS']);
    1520
    1521 // Start session
    1522 if (!$session->start('myapp')) {
    1523     die('Session start failed');
    1524 }
    1525
    1526 // Check for session validation messages
    1527 if ($message = $session->message()) {
    1528     error_log("Session validation: $message");
    1529 }
    1530
    1531 // Use session
    1532 if (!isset($session['user_id'])) {
    1533     // Handle login...
    1534     $session['user_id'] = $user->id;
    1535     $session['username'] = $user->name;
    1536     $session->regenerate_id(false, 'login');
    1537 } else {
    1538     // User already logged in
    1539     echo "Welcome back, " . htmlspecialchars($session['username']);
    1540 }
    1541
    1542 // Logout handling
    1543 if ($_REQUEST['action'] === 'logout') {
    1544     $session->restart();
    1545     header('Location: /');
    1546 }
    1547
    1548 // Automatic cleanup happens in register_shutdown_function()
    1549 ```
    1550
    1551 ### Configuration Best Practices
    1552
    1553 ```php
    1554 <?php
    1555
    1556 class SessionConfig {
    1557     public static function apply(Session $session, string $environment = 'production'): void {
    1558         // Base configuration
    1559         $session->cf_cookie_prefix = 'app_';
    1560         $session->cf_cookie_path = '/';
    1561         $session->cf_cache_limiter = 'private';
    1562
    1563         if ($environment === 'production') {
    1564             // Strict production settings
    1565             $session->cf_cookie_secure = true; // HTTPS only
    1566             $session->cf_cookie_httponly = true; // No JavaScript
    1567             $session->cf_cookie_samesite = 'Strict'; // Maximum CSRF protection
    1568             $session->cf_prevent_replay = true; // Anti-replay
    1569             $session->cf_max_session = 3600; // 1 hour
    1570             $session->cf_max_idle = 1800; // 30 minutes
    1571             $session->cf_regen_time = 300; // Regen every 5 min
    1572             $session->cf_regen_probability = 50; // 50% chance
    1573         } else {
    1574             // Development settings
    1575             $session->cf_cookie_secure = false; // Allow HTTP
    1576             $session->cf_cookie_httponly = false; // Allow JS debugging
    1577             $session->cf_prevent_replay = false; // Easier testing
    1578             $session->cf_max_session = 86400; // 24 hours
    1579             $session->cf_max_idle = 3600; // 1 hour
    1580             $session->cf_regen_time = 60; // 1 minute
    1581             $session->cf_regen_probability = 10; // 10% chance
    1582         }
    1583     }
    1584 }
    1585
    1586 // Usage
    1587 $session = new FileSession();
    1588 SessionConfig::apply($session, $_ENV['APP_ENV'] ?? 'production');
    1589 $session->start('myapp');
    1590 ```
    1591
    1592 ### Testing Tips
    1593
    1594 ```php
    1595 <?php
    1596
    1597 // Test nonce generation and verification
    1598 $nonce1 = $session->create_nonce('test_action', 60);
    1599 assert($session->verify_nonce('test_action', $nonce1) === true);
    1600
    1601 // Test single-use property
    1602 assert($session->verify_nonce('test_action', $nonce1) === false);
    1603
    1604 // Test expiration
    1605 $old_nonce = $session->create_nonce('expire_test', 1);
    1606 sleep(2);
    1607 assert($session->verify_nonce('expire_test', $old_nonce) === false);
    1608
    1609 // Test user agent validation
    1610 assert(isset($session->__user_agent));
    1611
    1612 // Test session ID format
    1613 assert(preg_match('/^[a-zA-Z0-9]{21}$/', $session->id()));
    1614
    1615 // Test data persistence
    1616 $session['test_key'] = 'test_value';
    1617 $session->write_close();
    1618 // New request...
    1619 $session2 = new FileSession();
    1620 $session2->start('myapp');
    1621 assert($session2['test_key'] === 'test_value');
    1622 ```
    1623
    1624 ---
    1625
    1626 ## Security Checklist
    1627
    1628 Use this checklist when implementing sessions:
    1629
    1630 - [ ] Use HTTPS only in production
    1631 - [ ] Enable `cf_cookie_secure`
    1632 - [ ] Enable `cf_cookie_httponly`
    1633 - [ ] Set `cf_cookie_samesite` to 'Strict' or 'Lax'
    1634 - [ ] Set appropriate `cf_max_session` timeout
    1635 - [ ] Set appropriate `cf_max_idle` timeout
    1636 - [ ] Enable `cf_prevent_replay`
    1637 - [ ] Validate `cf_ip` if possible
    1638 - [ ] Validate `cf_tls` on HTTPS sites
    1639 - [ ] Use nonces for all state-changing forms
    1640 - [ ] Implement proper logout (call `restart()`)
    1641 - [ ] Regenerate on privilege escalation (login)
    1642 - [ ] Monitor `sticky__ip` for suspicious changes
    1643 - [ ] Review `sticky__log` for attack patterns
    1644 - [ ] Implement garbage collection (`store_gc`)
    1645 - [ ] Hash session IDs before storing (see TODOs)
    1646 - [ ] Use secure random token generation
    1647
    1648 ---
    1649
    1650 ## Common Patterns
    1651
    1652 ### Login Flow
    1653
    1654 ```php
    1655 if ($_POST['action'] === 'login') {
    1656     $user = authenticate($_POST['username'], $_POST['password']);
    1657     if ($user) {
    1658         $session->regenerate_id(false, 'login'); // New ID after auth
    1659         $session['user_id'] = $user->id;
    1660         $session['username'] = $user->username;
    1661         $session['roles'] = $user->roles;
    1662         header('Location: /dashboard');
    1663     } else {
    1664         $session->set_flash('error', 'Invalid credentials', 1);
    1665         header('Location: /login');
    1666     }
    1667 }
    1668 ```
    1669
    1670 ### Logout Flow
    1671
    1672 ```php
    1673 if ($_GET['action'] === 'logout') {
    1674     $session->restart(); // Complete reset
    1675     header('Location: /');
    1676 }
    1677 ```
    1678
    1679 ### CSRF-Protected Form
    1680
    1681 ```php
    1682 // Display form
    1683 $csrf = $session->create_nonce('form_' . $form_id, 3600);
    1684 echo '<form method="POST">';
    1685 echo '<input type="hidden" name="csrf" value="' . htmlspecialchars($csrf) . '">';
    1686 // ... form fields
    1687 echo '</form>';
    1688
    1689 // Process form
    1690 if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    1691     if (!$session->verify_nonce('form_' . $form_id, $_POST['csrf'] ?? '')) {
    1692         die('CSRF check failed');
    1693     }
    1694     // Process safely
    1695 }
    1696 ```
    1697
    1698 ### Permission Check with Session Regeneration
    1699
    1700 ```php
    1701 if ($user->privilege_level < ADMIN_LEVEL && $promoted_to_admin) {
    1702     $session->regenerate_id(false, 'privilege_escalation');
    1703     $session['is_admin'] = true;
    1704 }
    1705 ```
    1706
    1707 ### Session Messages/Flash
    1708
    1709 ```php
    1710 // After action
    1711 $session->set_flash('info', 'Profile updated successfully', 1);
    1712
    1713 // Display next page
    1714 if (isset($session['info'])) {
    1715     echo $session['info'];
    1716 }
    1717 ```
    1718
    1719 ---
    1720
    1721 ## Performance Considerations
    1722
    1723 ### Optimization Tips
    1724
    1725 1. **Minimize Session Writes:**
    1726    - Session data only written during `write_close()` or regeneration
    1727    - No unnecessary serialization during reads
    1728
    1729 2. **Garbage Collection:**
    1730    - Probabilistic GC (based on `cf_gc_probability`)
    1731    - Only runs on ~2% of requests by default
    1732    - Customize based on your session volume
    1733
    1734 3. **Nonce Cleanup:**
    1735    - Expired nonces automatically removed on verification
    1736    - Verified nonces removed from storage
    1737    - No manual cleanup needed
    1738
    1739 4. **Session ID Validation:**
    1740    - Regex-based validation is fast
    1741    - No database lookup needed
    1742
    1743 5. **Caching Strategy:**
    1744    - Cache expensive lookups between session operations
    1745    - Session data loaded once per request
    1746
    1747 ### Benchmarks
    1748
    1749 Typical performance on modern hardware:
    1750
    1751 - Session start: ~1-5ms (file) / ~2-10ms (database)
    1752 - Session write: <1ms (file) / 1-5ms (database)
    1753 - Nonce generation: <1ms
    1754 - Nonce verification: <1ms
    1755
    1756 ---
    1757
    1758 ## Troubleshooting
    1759
    1760 ### Session Not Starting
    1761
    1762 ```php
    1763 if (!$session->start('myapp')) {
    1764     // Check reasons:
    1765     // 1. Headers already sent?
    1766     // 2. Storage backend not initialized?
    1767     // 3. Permissions issue on session directory?
    1768     debug_backtrace();
    1769 }
    1770 ```
    1771
    1772 ### Cookie Not Setting
    1773
    1774 ```php
    1775 // If setcookie() returns false:
    1776 // - Check if headers_sent()
    1777 // - Check if cookie name is RFC 2616 compliant
    1778 // - Check if cookie value is properly encoded
    1779 ```
    1780
    1781 ### Session ID Not Regenerating
    1782
    1783 ```php
    1784 // If regenerate_id() returns false:
    1785 // - Headers might be sent
    1786 // - $active might be false
    1787 // - Already regenerated once in this request
    1788 if (!$session->regenerate_id()) {
    1789     error_log("Regeneration failed: headers sent or session inactive");
    1790 }
    1791 ```
    1792
    1793 ### Nonce Verification Failing
    1794
    1795 ```php
    1796 // If verify_nonce() returns false:
    1797 // 1. Nonce might be expired
    1798 // 2. Nonce might be for different action
    1799 // 3. Nonce might have been used already
    1800 // 4. Session might have been reset
    1801
    1802 // Debug:
    1803 var_dump($session->__nonces); // See stored nonces
    1804 ```
    1805
    1806 ### Session Data Lost
    1807
    1808 ```php
    1809 // Possible causes:
    1810 // 1. write_close() not called (usually automatic via shutdown)
    1811 // 2. Storage backend failing silently
    1812 // 3. File permissions issues
    1813 // 4. Session timeout due to cf_max_idle
    1814 // 5. IP/UA/TLS validation failure (check message())
    1815
    1816 if ($message = $session->message()) {
    1817     error_log("Session issue: $message");
    1818 }
    1819 ```
    1820
    1821 ---
    1822
    1823 ## TODO Items (From Code Comments)
    1824
    1825 The following improvements are planned:
    1826
    1827 1. **Do not store session ID in filename or DB index - store hash instead**
    1828    - Improves security by not exposing IDs in storage layer
    1829    - Would require hashing logic in store_* methods
    1830
    1831 2. **Log of IP changes and other possible security alerts**
    1832    - Track `sticky__ip` changes more comprehensively
    1833    - Create security audit trail
    1834
    1835 3. **Allocate internal unique session which lives through lifetime of uber-session**
    1836    - Multi-session management (parent/child sessions)
    1837    - Useful for complex user flows
    1838
    1839 4. **Do not delete old sessions, but use them as hijack pointers**
    1840    - Maintain session history for analysis
    1841    - Detect potential session hijacking patterns
    1842    - Implement session relationship tracking
    1843
    1844 5. **All SIDs used later than ~5secs of regenerations is hijacks**
    1845    - Detect and block delayed session ID usage
    1846    - Current implementation allows 5-second window
    1847    - Could be more granular
    1848
    1849 ---
    1850
    1851 ## References
    1852
    1853 ### Security Standards
    1854
    1855 - RFC 2616: HTTP/1.1 (Cookie syntax)
    1856 - RFC 6265: HTTP State Management Mechanism
    1857 - RFC 6234: US Secure Hash and Message Authentication Code Algorithms
    1858 - OWASP: Session Management Cheat Sheet
    1859 - OWASP: Cross-Site Request Forgery (CSRF) Prevention
    1860
    1861 ### Related Code
    1862
    1863 - `Ut::serialize()` / `Ut::unserialize()`: Session data serialization
    1864 - `Ut::random_token()`: Cryptographic token generation
    1865 - `Ut::http_date()`: HTTP date formatting
    1866 - `Ut::urlencode()`: Cookie-safe encoding
    1867 - `Ut::is_empty()`: Empty value checking
    1868
    1869 ### See Also
    1870
    1871 - `src/class/http.php`: HTTP request/response handling
    1872 - `src/class/auth.php`: Authentication (uses Session)
    1873 - Session security best practices in OWASP documentation
    1874
    1875 ---
    1876
    1877 ## Version History
    1878
    1879 - **Current**: Abstract session class with security features
    1880 - **Planned**: Implementation of TODO items above
    1881
    1882 ---
    1883
    1884 *Documentation generated: 2026-05-05*
    1885 *For latest updates, see: https://github.com/Trojer/wackowiki/blob/main/docs/SESSION_DOCUMENTATION.md*