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