@@ -21,6 +21,7 @@ class Session implements SessionHandlerInterface
2121 private bool $ autoCommit = true ;
2222 private bool $ testMode = false ;
2323 private bool $ inRegenerate = false ;
24+ private ?string $ serialization = null ; // 'json' (default) or 'php'
2425
2526 /**
2627 * Constructor to initialize the session handler.
@@ -41,6 +42,11 @@ public function __construct(array $config = [])
4142 $ this ->autoCommit = $ config ['auto_commit ' ] ?? true ;
4243 $ startSession = $ config ['start_session ' ] ?? true ;
4344 $ this ->testMode = $ config ['test_mode ' ] ?? false ;
45+ $ this ->serialization = $ config ['serialization ' ] ?? 'json ' ; // 'json' (default) or 'php'
46+
47+ if (!in_array ($ this ->serialization , ['json ' , 'php ' ], true )) {
48+ throw new \InvalidArgumentException ("Invalid serialization method: {$ this ->serialization }. Use 'json' or 'php'. " );
49+ }
4450
4551 // Set test session ID if provided
4652 if ($ this ->testMode === true && isset ($ config ['test_session_id ' ])) {
@@ -138,47 +144,60 @@ public function read($id): string
138144 $ this ->sessionId = $ id ;
139145 $ file = $ this ->getSessionFile ($ id );
140146
141- // Fail fast: no file exists
142- if (file_exists ($ file ) === false ) {
147+ if (file_exists ($ file ) !== true ) {
143148 $ this ->data = [];
144- return '' ; // Return empty string for new sessions
149+ return '' ;
145150 }
146151
147- // Fail fast: unable to read file or empty content
148152 $ content = file_get_contents ($ file );
149153 if ($ content === false || strlen ($ content ) < 1 ) {
150154 $ this ->data = [];
151155 return '' ;
152156 }
153157
154- // Extract prefix and data
155158 $ prefix = $ content [0 ];
156159 $ dataStr = substr ($ content , 1 );
157160
158- // Handle plain data (no encryption)
159- if ($ prefix === 'P ' && $ this ->encryptionKey === null ) {
160- $ unserialized = unserialize ($ dataStr );
161- if ($ unserialized !== false ) {
162- $ this ->data = $ unserialized ;
163- return '' ; // Return empty string to let PHP handle serialization
164- }
165- }
166-
167- // Handle encrypted data
168- if ($ prefix === 'E ' && $ this ->encryptionKey !== null ) {
161+ if ($ this ->serialization === 'json ' ) {
162+ if ($ prefix === 'J ' && $ this ->encryptionKey === null ) {
163+ $ decoded = json_decode ($ dataStr , true );
164+ if (json_last_error () === JSON_ERROR_NONE ) {
165+ $ this ->data = $ decoded ;
166+ return '' ;
167+ }
168+ } elseif ($ prefix === 'F ' && $ this ->encryptionKey !== null ) {
169169 $ iv = substr ($ dataStr , 0 , 16 );
170170 $ encrypted = substr ($ dataStr , 16 );
171171 $ decrypted = openssl_decrypt ($ encrypted , 'AES-256-CBC ' , $ this ->encryptionKey , 0 , $ iv );
172-
173- if ($ decrypted !== false ) {
174- $ unserialized = unserialize ($ decrypted );
172+ if ($ decrypted !== false ) {
173+ $ decoded = json_decode ($ decrypted , true );
174+ if (json_last_error () === JSON_ERROR_NONE ) {
175+ $ this ->data = $ decoded ;
176+ return '' ;
177+ }
178+ }
179+ }
180+ } elseif ($ this ->serialization === 'php ' ) {
181+ if ($ prefix === 'P ' && $ this ->encryptionKey === null ) {
182+ $ unserialized = unserialize ($ dataStr );
175183 if ($ unserialized !== false ) {
176184 $ this ->data = $ unserialized ;
177185 return '' ;
178186 }
187+ } elseif ($ prefix === 'E ' && $ this ->encryptionKey !== null ) {
188+ $ iv = substr ($ dataStr , 0 , 16 );
189+ $ encrypted = substr ($ dataStr , 16 );
190+ $ decrypted = openssl_decrypt ($ encrypted , 'AES-256-CBC ' , $ this ->encryptionKey , 0 , $ iv );
191+ if ($ decrypted !== false ) {
192+ $ unserialized = unserialize ($ decrypted );
193+ if ($ unserialized !== false ) {
194+ $ this ->data = $ unserialized ;
195+ return '' ;
196+ }
197+ }
179198 }
180199 }
181- // Fail fast: mismatch between prefix and encryption state or corruption
200+ // Fail fast: mismatch or corruption
182201 $ this ->data = [];
183202 return '' ;
184203 }
@@ -207,32 +226,46 @@ protected function encryptData(string $data)
207226 */
208227 public function write ($ id , $ data ): bool
209228 {
210- // When PHP calls this method, it passes serialized data
211- // We ignore this parameter because we maintain our data internally
212- // and handle serialization ourselves
213-
214- // Fail fast: no changes to write
215- if ($ this ->changed === false && empty ($ this ->data ) === false ) {
229+ if ($ this ->changed !== true && !empty ($ this ->data )) {
216230 return true ;
217231 }
218232
219233 $ file = $ this ->getSessionFile ($ id );
220- $ serialized = serialize ($ this ->data );
221-
222- // Handle encryption if key is provided
223- if ($ this ->encryptionKey !== null ) {
224- $ content = $ this ->encryptData ($ serialized );
225234
226- // Fail fast: encryption failed
227- if ($ content === false ) {
228- return false ;
235+ if ($ this ->serialization === 'json ' ) {
236+ if (!empty ($ this ->data )) {
237+ $ this ->assertNoObjects ($ this ->data );
238+ }
239+ $ serialized = json_encode ($ this ->data );
240+ if ($ serialized === false ) {
241+ return false ; // @codeCoverageIgnore
242+ }
243+ if ($ this ->encryptionKey !== null ) {
244+ $ iv = openssl_random_pseudo_bytes (16 );
245+ $ encrypted = openssl_encrypt ($ serialized , 'AES-256-CBC ' , $ this ->encryptionKey , 0 , $ iv );
246+ if ($ encrypted === false ) {
247+ return false ; // @codeCoverageIgnore
248+ }
249+ $ content = 'F ' . $ iv . $ encrypted ;
250+ } else {
251+ $ content = 'J ' . $ serialized ;
252+ }
253+ } elseif ($ this ->serialization === 'php ' ) {
254+ $ serialized = serialize ($ this ->data );
255+ if ($ this ->encryptionKey !== null ) {
256+ $ content = $ this ->encryptData ($ serialized ); // returns 'E' . $iv . $encrypted
257+ if ($ content === false ) {
258+ return false ; // @codeCoverageIgnore
259+ }
260+ } else {
261+ $ content = 'P ' . $ serialized ;
229262 }
230263 } else {
231- $ content = 'P ' . $ serialized ;
264+ // Should never happen
265+ return false ; // @codeCoverageIgnore
232266 }
233267
234- // Write to file and return success
235- return file_put_contents ($ file , $ content ) !== false ;
268+ return file_put_contents ($ file , $ content ) !== false ; // @codeCoverageIgnore
236269 }
237270
238271 /**
@@ -430,4 +463,26 @@ private function getSessionFile(string $id): string
430463 {
431464 return $ this ->savePath . '/ ' . $ this ->prefix . $ id ;
432465 }
466+
467+ /**
468+ * Recursively check for objects in data (for JSON safety).
469+ * Throws exception if object is found.
470+ * @param mixed $data
471+ * @throws \InvalidArgumentException
472+ */
473+ private function assertNoObjects ($ data ): void
474+ {
475+ // Iterative stack to avoid recursion
476+ $ stack = [$ data ];
477+ while ($ stack ) {
478+ $ current = array_pop ($ stack );
479+ foreach ($ current as $ v ) {
480+ if (is_object ($ v ) === true ) {
481+ throw new \InvalidArgumentException ('Session data contains an object, which cannot be safely stored with JSON serialization. ' );
482+ } elseif (is_array ($ v ) === true ) {
483+ $ stack [] = $ v ;
484+ }
485+ }
486+ }
487+ }
433488}
0 commit comments