Difference between revisions for Users / Eo Ny
|
← Previous edit
|
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. |
| … | … |
| 36 | |
| 37 | ==== Session Data Storage ==== |
| 38 | Session data is stored as an array accessible through ##ArrayObject## interface: |
| 39 | %% |
| 40 | $session['user_id'] = 123; // Set data |
| 41 | echo $session['user_id']; // Get data |
| 42 | %% |
| … | … |
| 112 | All configuration properties are prefixed with ##cf_## (config) and can be set before calling ##start()##: |
| 113 | |
| 114 | ===== Session Behavior ===== |
| 115 | %% |
| 116 | $session->cf_static = 0; // Disable regenerations (e.g., for CAPTCHA) |
| 117 | $session->cf_max_session = 7200; // Max session lifetime (seconds) |
| 118 | $session->cf_max_idle = 1440; // Max idle time before destruction (seconds) |
| … | … |
| 121 | %% |
| 122 | |
| 123 | ===== Nonce & Replay Protection ===== |
| 124 | %% |
| 125 | $session->cf_secret = 'adyaiD9+255JeiskPybgisby'; // Secret for nonce generation |
| 126 | $session->cf_nonce_lifetime = 7200; // Nonce expiration (seconds) |
| 127 | $session->cf_prevent_replay = 1; // Enable replay attack prevention |
| 128 | %% |
| 129 | |
| 130 | ===== Garbage Collection ===== |
| 131 | %% |
| 132 | $session->cf_gc_probability = 2; // Probability of GC on shutdown (0-100) |
| 133 | $session->cf_gc_maxlifetime = 1440; // Max session file lifetime (seconds) |
| 134 | %% |
| 135 | |
| 136 | ===== Cookie Settings ===== |
| 137 | %% |
| 138 | $session->cf_cookie_prefix = ''; // Prefix for all cookies |
| 139 | $session->cf_cookie_persistent = false; // Make cookies persistent |
| 140 | $session->cf_cookie_lifetime = 0; // Cookie lifetime (0 = session cookie) |
| … | … |
| 146 | %% |
| 147 | |
| 148 | ===== Cache Control ===== |
| 149 | %% |
| 150 | $session->cf_cache_limiter = 'none'; // Cache control mode (public|private|nocache|none) |
| 151 | $session->cf_cache_expire = 180*60; // Cache TTL (seconds) |
| 152 | $session->cf_cache_mtime = 0; // Modify time for Last-Modified header |
| 153 | %% |
| 154 | |
| 155 | ===== Security Validation ===== |
| 156 | %% |
| 157 | $session->cf_referer_check = ''; // Check HTTP Referer header |
| 158 | %% |
| 159 | |
| 160 | ===== HTTP Context (Set by HTTP class) ===== |
| 161 | %% |
| 162 | $session->cf_ip; // Client IP address |
| 163 | $session->cf_tls; // TLS/SSL connection indicator |
| 164 | %% |
| … | … |
| 169 | |
| 170 | ==== Basic Session Setup ==== |
| 171 | |
| 172 | %% |
| 173 | // Create a concrete session implementation |
| 174 | class MySession extends Session { |
| 175 | // Implement abstract store_* methods |
| … | … |
| 202 | |
| 203 | ==== Session Data Access ==== |
| 204 | |
| 205 | %% |
| 206 | // Array-like access (via ArrayObject) |
| 207 | $session['user_id'] = 123; |
| 208 | echo $session['user_id']; |
| … | … |
| 215 | |
| 216 | ==== Session ID Management ==== |
| 217 | |
| 218 | %% |
| 219 | // Get current session ID |
| 220 | $id = $session->id(); // Returns: e.g., "abc123xyz..." |
| 221 | |
| … | … |
| 228 | |
| 229 | ==== Session State ==== |
| 230 | |
| 231 | %% |
| 232 | // Check if session is active |
| 233 | if ($session->active()) { |
| 234 | // Session is running |
| … | … |
| 256 | - Session validation failures |
| 257 | |
| 258 | **Manual Trigger:** |
| 259 | %% |
| 260 | $session->regenerate_id($delete_old = false, $message = 'custom_reason'); |
| 261 | %% |
| 262 | |
| … | … |
| 274 | - Single regeneration per request (checked via ##$this->regenerated## flag) |
| 275 | - Logged in ##sticky__log## for debugging (max 15 entries) |
| 276 | |
| 277 | %% |
| 278 | // Example: Force regeneration on login |
| 279 | $session->start('myapp'); |
| 280 | if ($user_authenticated) { |
| … | … |
| 294 | - Useful against bot attacks or stolen sessions |
| 295 | |
| 296 | **Configuration:** |
| 297 | %% |
| 298 | // Automatic on each request (if enabled in code logic) |
| 299 | // Triggers session destruction if UA changes significantly |
| 300 | %% |
| … | … |
| 310 | - Tracks IP changes in ##sticky__ip## |
| 311 | |
| 312 | **Configuration:** |
| 313 | %% |
| 314 | $session->cf_ip = $_SERVER['REMOTE_ADDR']; // Set by HTTP class |
| 315 | // Validation happens automatically during start() |
| 316 | %% |
| 317 | |
| 318 | **IP Change Tracking:** |
| 319 | %% |
| 320 | // Access IP change history |
| 321 | $ip_history = $session->sticky__ip; // Array of [ip => change_count] |
| 322 | %% |
| … | … |
| 330 | - Destroys session on mismatch |
| 331 | |
| 332 | **Configuration:** |
| 333 | %% |
| 334 | $session->cf_tls = !empty($_SERVER['HTTPS']); // Set by HTTP class |
| 335 | // Validation happens automatically during start() |
| 336 | %% |
| … | … |
| 345 | - Detects rapid-fire requests (AJAX attacks) |
| 346 | |
| 347 | **Configuration:** |
| 348 | %% |
| 349 | $session->cf_prevent_replay = 1; // Enable (default) |
| 350 | $session->cf_prevent_replay = 0; // Disable if needed |
| 351 | %% |
| … | … |
| 362 | **Purpose:** Prevent CSRF via header checking |
| 363 | |
| 364 | **Configuration:** |
| 365 | %% |
| 366 | $session->cf_referer_check = 'example.com'; |
| 367 | // Session rejected if HTTP_REFERER doesn't contain this string |
| 368 | %% |
| … | … |
| 391 | - May trigger session ID regeneration |
| 392 | |
| 393 | **Example:** |
| 394 | %% |
| 395 | if ($session->start('webapp', $_COOKIE['sess_id'] ?? null)) { |
| 396 | // Session ready |
| 397 | } else { |
| … | … |
| 421 | - Sets ##$active = false## |
| 422 | |
| 423 | **Example:** |
| 424 | %% |
| 425 | $session['key'] = 'value'; |
| 426 | $session->write_close(); // Ensure data is saved |
| 427 | %% |
| … | … |
| 441 | - Complete session refresh |
| 442 | |
| 443 | **Example:** |
| 444 | %% |
| 445 | $session->restart(); |
| 446 | // New session created, old data cleared, sticky_ vars preserved |
| 447 | %% |
| … | … |
| 455 | |
| 456 | **Returns:** Session ID string or null if not started |
| 457 | |
| 458 | %% |
| 459 | $sid = $session->id(); // "abc123xyz..." |
| 460 | %% |
| 461 | |
| … | … |
| 466 | |
| 467 | **Returns:** Session name |
| 468 | |
| 469 | %% |
| 470 | $name = $session->name(); // "myapp" |
| 471 | %% |
| 472 | |
| … | … |
| 477 | |
| 478 | **Returns:** ##true## if session is started and active, ##false## otherwise |
| 479 | |
| 480 | %% |
| 481 | if ($session->active()) { |
| 482 | $session['key'] = 'value'; |
| 483 | } |
| … | … |
| 503 | - ##null##: No state change |
| 504 | |
| 505 | **Example:** |
| 506 | %% |
| 507 | $session->start('app'); |
| 508 | if ($message = $session->message()) { |
| 509 | error_log("Session issue: $message"); |
| … | … |
| 519 | |
| 520 | **Note:** This is a direct call to ##ArrayObject::getArrayCopy()## |
| 521 | |
| 522 | %% |
| 523 | $data = $session->toArray(); |
| 524 | foreach ($data as $key => $value) { |
| 525 | echo "$key => $value\n"; |
| … | … |
| 540 | **Returns:** Nonce token string (11 characters) |
| 541 | |
| 542 | **Example:** |
| 543 | %% |
| 544 | $nonce = $session->create_nonce('form_submit', 3600); |
| 545 | // Use in HTML: <input type="hidden" name="nonce" value="<?= $nonce ?>"> |
| 546 | %% |
| … | … |
| 568 | - ##-1##: Protected nonce used twice in quick succession (possible AJAX attack) |
| 569 | |
| 570 | **Example:** |
| 571 | %% |
| 572 | if ($nonce = $session->verify_nonce('form_submit', $_POST['nonce'])) { |
| 573 | if ($nonce === -1) { |
| 574 | // Possible replay, but might be legitimate AJAX |
| … | … |
| 611 | - Does NOT replace existing cookies (allows multiple Set-Cookie headers) |
| 612 | |
| 613 | **Example:** |
| 614 | %% |
| 615 | // Session cookie |
| 616 | $session->setcookie('user_pref', 'dark_mode'); |
| 617 | |
| … | … |
| 636 | |
| 637 | **Returns:** Cookie value or null if not set |
| 638 | |
| 639 | %% |
| 640 | $value = $session->get_cookie('user_pref'); // Reads $_COOKIE['user_pref'] |
| 641 | %% |
| 642 | |
| … | … |
| 654 | - ##0##: Use ##cf_cookie_persistent## config |
| 655 | |
| 656 | **Example:** |
| 657 | %% |
| 658 | $session->set_cookie('theme', 'dark'); // Session cookie |
| 659 | $session->set_cookie('lang', 'en', 365); // 1 year |
| 660 | %% |
| … | … |
| 669 | |
| 670 | **Implementation:** Sets empty value with immediate expiration |
| 671 | |
| 672 | %% |
| 673 | $session->delete_cookie('old_preference'); |
| 674 | %% |
| 675 | |
| … | … |
| 678 | ====== ##unsetcookie($name): void## ====== |
| 679 | Alias for ##setcookie($name)## with no value (convenience method). |
| 680 | |
| 681 | %% |
| 682 | $session->unsetcookie('cookie_name'); |
| 683 | %% |
| 684 | |
| … | … |
| 699 | **Default Implementation:** Returns 21-character random alphanumeric string via ##Ut::random_token(21)## |
| 700 | |
| 701 | **Override in subclass to customize:** |
| 702 | %% |
| 703 | protected function store_generate_id(): string { |
| 704 | return hash('sha256', random_bytes(32)); // Your format |
| 705 | } |
| … | … |
| 713 | **Default Implementation:** Regex check: ##/^[a-zA-Z\d]{21}$/## |
| 714 | |
| 715 | **Override in subclass to match your format:** |
| 716 | %% |
| 717 | protected function store_validate_id($id): bool { |
| 718 | return preg_match('/^[a-f0-9]{64}$/', $id); // SHA256 format |
| 719 | } |
| … | … |
| 727 | **Subclass must implement** - Initialize storage handler |
| 728 | |
| 729 | **Example:** |
| 730 | %% |
| 731 | protected function store_open($name): void { |
| 732 | $this->db = new PDO('sqlite::memory:'); |
| 733 | } |
| … | … |
| 750 | - ##false## if session doesn't exist or read error |
| 751 | |
| 752 | **Example:** |
| 753 | %% |
| 754 | protected function store_read($id, $lock = false): string|false { |
| 755 | $data = file_get_contents("/tmp/sess_$id"); |
| 756 | return $data ?: false; |
| … | … |
| 769 | - ##$data##: Serialized session data (already processed by ##Ut::serialize()##) |
| 770 | |
| 771 | **Example:** |
| 772 | %% |
| 773 | protected function store_write($id, $data): void { |
| 774 | file_put_contents("/tmp/sess_$id", $data); |
| 775 | } |
| … | … |
| 783 | **Subclass must implement** - Release resources |
| 784 | |
| 785 | **Example:** |
| 786 | %% |
| 787 | protected function store_close(): void { |
| 788 | // Close database, file, etc. |
| 789 | } |
| … | … |
| 803 | - Sessions older than ##cf_gc_maxlifetime## seconds |
| 804 | |
| 805 | **Example:** |
| 806 | %% |
| 807 | protected function store_gc(): void { |
| 808 | $max_age = time() - $this->cf_gc_maxlifetime; |
| 809 | // Delete files/records older than $max_age |
| … | … |
| 1038 | |
| 1039 | ==== Usage ==== |
| 1040 | |
| 1041 | %% |
| 1042 | // Store flash message for next request |
| 1043 | $session->set_flash('error', 'Username already exists', 1); // 1 request |
| 1044 | $session->set_flash('info', 'Welcome back!', 2); // 2 requests |
| … | … |
| 1052 | - Key: Variable name |
| 1053 | - Value: Lifetime in requests |
| 1054 | 2. **Cleanup:** In ##terminator()## (shutdown handler): |
| 1055 | %% |
| 1056 | foreach ($sticky__flash as $var => $age) { |
| 1057 | if (!isset($session[$var])) { |
| 1058 | unset($sticky__flash[$var]); // Already deleted |
| … | … |
| 1068 | |
| 1069 | ==== Example: Login Flow ==== |
| 1070 | |
| 1071 | %% |
| 1072 | // POST /login |
| 1073 | if ($credentials_valid) { |
| 1074 | $session->restart(); // New session |
| … | … |
| 1102 | |
| 1103 | ==== Complete Example: Form Protection ==== |
| 1104 | |
| 1105 | %% |
| 1106 | // 1. Display form with nonce |
| 1107 | $nonce = $session->create_nonce('user_update', 3600); |
| 1108 | ?> |
| … | … |
| 1127 | |
| 1128 | ==== Example: Protected Nonce (AJAX-Safe) ==== |
| 1129 | |
| 1130 | %% |
| 1131 | // Generate protected nonce (can verify multiple times) |
| 1132 | $nonce = $session->create_nonce('ajax_action', 300); |
| 1133 | |
| … | … |
| 1181 | The ##setcookie()## method implements comprehensive cookie security: |
| 1182 | |
| 1183 | ===== Encoding ===== |
| 1184 | %% |
| 1185 | // Cookie names: RFC 2616 2.2 token format |
| 1186 | // Cookie values: RFC 6265 4.1.1 cookie-octet format |
| 1187 | // Unsafe characters automatically URL-encoded |
| 1188 | %% |
| 1189 | |
| 1190 | ===== Security Attributes ===== |
| 1191 | %% |
| 1192 | setcookie('auth', 'token', |
| 1193 | expires: time() + 3600, |
| 1194 | secure: true, // HTTPS only |
| … | … |
| 1198 | %% |
| 1199 | |
| 1200 | ===== No Duplicate Headers ===== |
| 1201 | %% |
| 1202 | // Automatically removes old Set-Cookie header before setting new one |
| 1203 | // Prevents cookie header duplication |
| 1204 | remove_cookie($name) → clears old headers |
| … | … |
| 1207 | |
| 1208 | ==== Configuration-Driven Defaults ==== |
| 1209 | |
| 1210 | %% |
| 1211 | $session->cf_cookie_path = '/app'; // Path |
| 1212 | $session->cf_cookie_domain = '.example.com'; // Domain |
| 1213 | $session->cf_cookie_secure = true; // HTTPS |
| … | … |
| 1221 | |
| 1222 | ==== Typical Secure Configuration ==== |
| 1223 | |
| 1224 | %% |
| 1225 | // Prevent XSS and CSRF |
| 1226 | $session->cf_cookie_secure = true; // HTTPS only |
| 1227 | $session->cf_cookie_httponly = true; // No JavaScript access |
| … | … |
| 1245 | The Session class gracefully handles errors: |
| 1246 | |
| 1247 | ===== Headers Already Sent ===== |
| 1248 | %% |
| 1249 | if (headers_sent($file, $line)) { |
| 1250 | trigger_error("id regeneration requested after headers flushed at $file:$line", |
| 1251 | E_USER_WARNING); |
| … | … |
| 1256 | **Impact:** Session ID cannot be regenerated, but session continues |
| 1257 | |
| 1258 | ===== Cookie Setting Failure ===== |
| 1259 | %% |
| 1260 | if (headers_sent($file, $line)) { |
| 1261 | trigger_error("cannot place session cookie $name=$value due to $file:$line", |
| 1262 | E_USER_WARNING); |
| … | … |
| 1267 | **Impact:** Cookie not set, but session data remains accessible |
| 1268 | |
| 1269 | ===== Storage Errors ===== |
| 1270 | %% |
| 1271 | if ($this->store_read($this->id, true) !== '') { |
| 1272 | // error! [comment indicates error, but continues] |
| 1273 | } |
| … | … |
| 1279 | |
| 1280 | The Session class includes commented debug statements: |
| 1281 | |
| 1282 | %% |
| 1283 | # Ut::dbg("regeneration failed by flush at $file:$line"); |
| 1284 | # Ut::dbg($destroy, $message); |
| 1285 | # Ut::dbg("session setcookie $name failed by $file:$line"); |
| … | … |
| 1291 | |
| 1292 | Session events tracked in ##sticky__log##: |
| 1293 | |
| 1294 | %% |
| 1295 | // Access session event history |
| 1296 | if (isset($session->sticky__log)) { |
| 1297 | foreach ($session->sticky__log as [$timestamp, $message]) { |
| … | … |
| 1314 | |
| 1315 | ===== File-Based Storage ===== |
| 1316 | |
| 1317 | %% |
| 1318 | <?php |
| 1319 | |
| 1320 | class FileSession extends Session { |
| … | … |
| 1375 | |
| 1376 | ===== Database Storage (PDO) ===== |
| 1377 | |
| 1378 | %% |
| 1379 | <?php |
| 1380 | |
| 1381 | class DatabaseSession extends Session { |
| … | … |
| 1443 | |
| 1444 | ===== Redis Storage ===== |
| 1445 | |
| 1446 | %% |
| 1447 | <?php |
| 1448 | |
| 1449 | class RedisSession extends Session { |
| … | … |
| 1493 | |
| 1494 | ==== Complete Integration Example ==== |
| 1495 | |
| 1496 | %% |
| 1497 | <?php |
| 1498 | |
| 1499 | // Initialize session with configuration |
| … | … |
| 1543 | |
| 1544 | ==== Configuration Best Practices ==== |
| 1545 | |
| 1546 | %% |
| 1547 | <?php |
| 1548 | |
| 1549 | class SessionConfig { |
| … | … |
| 1584 | |
| 1585 | ==== Testing Tips ==== |
| 1586 | |
| 1587 | %% |
| 1588 | <?php |
| 1589 | |
| 1590 | // Test nonce generation and verification |
| … | … |
| 1643 | |
| 1644 | ==== Login Flow ==== |
| 1645 | |
| 1646 | %% |
| 1647 | if ($_POST['action'] === 'login') { |
| 1648 | $user = authenticate($_POST['username'], $_POST['password']); |
| 1649 | if ($user) { |
| … | … |
| 1661 | |
| 1662 | ==== Logout Flow ==== |
| 1663 | |
| 1664 | %% |
| 1665 | if ($_GET['action'] === 'logout') { |
| 1666 | $session->restart(); // Complete reset |
| 1667 | header('Location: /'); |
| … | … |
| 1670 | |
| 1671 | ==== CSRF-Protected Form ==== |
| 1672 | |
| 1673 | %% |
| 1674 | // Display form |
| 1675 | $csrf = $session->create_nonce('form_' . $form_id, 3600); |
| 1676 | echo '<form method="POST">'; |
| … | … |
| 1689 | |
| 1690 | ==== Permission Check with Session Regeneration ==== |
| 1691 | |
| 1692 | %% |
| 1693 | if ($user->privilege_level < ADMIN_LEVEL && $promoted_to_admin) { |
| 1694 | $session->regenerate_id(false, 'privilege_escalation'); |
| 1695 | $session['is_admin'] = true; |
| … | … |
| 1698 | |
| 1699 | ==== Session Messages/Flash ==== |
| 1700 | |
| 1701 | %% |
| 1702 | // After action |
| 1703 | $session->set_flash('info', 'Profile updated successfully', 1); |
| 1704 | |
| … | … |
| 1745 | |
| 1746 | ==== Session Not Starting ==== |
| 1747 | |
| 1748 | %% |
| 1749 | if (!$session->start('myapp')) { |
| 1750 | // Check reasons: |
| 1751 | // 1. Headers already sent? |
| … | … |
| 1757 | |
| 1758 | ==== Cookie Not Setting ==== |
| 1759 | |
| 1760 | %% |
| 1761 | // If setcookie() returns false: |
| 1762 | // - Check if headers_sent() |
| 1763 | // - Check if cookie name is RFC 2616 compliant |
| … | … |
| 1766 | |
| 1767 | ==== Session ID Not Regenerating ==== |
| 1768 | |
| 1769 | %% |
| 1770 | // If regenerate_id() returns false: |
| 1771 | // - Headers might be sent |
| 1772 | // - $active might be false |
| … | … |
| 1778 | |
| 1779 | ==== Nonce Verification Failing ==== |
| 1780 | |
| 1781 | %% |
| 1782 | // If verify_nonce() returns false: |
| 1783 | // 1. Nonce might be expired |
| 1784 | // 2. Nonce might be for different action |
| … | … |
| 1791 | |
| 1792 | ==== Session Data Lost ==== |
| 1793 | |
| 1794 | %% |
| 1795 | // Possible causes: |
| 1796 | // 1. write_close() not called (usually automatic via shutdown) |
| 1797 | // 2. Storage backend failing silently |