Difference between revisions for Users / Eo Ny





Next edit →

Version1 Version2 Differences
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*