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