Difference between revisions for Users / Eo Ny





Next edit →

Merge of Version1 & Version2
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*