@@ -27,6 +27,7 @@ import (
2727 "strings"
2828 "sync"
2929 "testing"
30+ "time"
3031
3132 "github.com/dunglas/frankenphp"
3233 "github.com/dunglas/frankenphp/internal/fastabs"
@@ -1306,3 +1307,108 @@ func TestIniPreLoopPreserved_worker(t *testing.T) {
13061307 realServer : true ,
13071308 })
13081309}
1310+
1311+ func TestSessionNoLeakBetweenRequests_worker (t * testing.T ) {
1312+ runTest (t , func (_ func (http.ResponseWriter , * http.Request ), ts * httptest.Server , i int ) {
1313+ // Client A: Set a secret value in session
1314+ clientA := & http.Client {}
1315+ resp1 , err := clientA .Get (ts .URL + "/session-leak.php?action=set&value=secret_A&client_id=clientA" )
1316+ assert .NoError (t , err )
1317+ body1 , _ := io .ReadAll (resp1 .Body )
1318+ _ = resp1 .Body .Close ()
1319+
1320+ body1Str := string (body1 )
1321+ t .Logf ("Client A set session: %s" , body1Str )
1322+ assert .Contains (t , body1Str , "SESSION_SET" )
1323+ assert .Contains (t , body1Str , "secret=secret_A" )
1324+
1325+ // Client B: Check that session is empty (no cookie, should not see Client A's data)
1326+ clientB := & http.Client {}
1327+ resp2 , err := clientB .Get (ts .URL + "/session-leak.php?action=check_empty" )
1328+ assert .NoError (t , err )
1329+ body2 , _ := io .ReadAll (resp2 .Body )
1330+ _ = resp2 .Body .Close ()
1331+
1332+ body2Str := string (body2 )
1333+ t .Logf ("Client B check empty: %s" , body2Str )
1334+ assert .Contains (t , body2Str , "SESSION_CHECK" )
1335+ assert .Contains (t , body2Str , "SESSION_EMPTY=true" ,
1336+ "Client B should have empty session, not see Client A's data.\n Response: %s" , body2Str )
1337+ assert .NotContains (t , body2Str , "secret_A" ,
1338+ "Client A's secret should not leak to Client B.\n Response: %s" , body2Str )
1339+
1340+ // Client C: Read session without cookie (should also be empty)
1341+ clientC := & http.Client {}
1342+ resp3 , err := clientC .Get (ts .URL + "/session-leak.php?action=get" )
1343+ assert .NoError (t , err )
1344+ body3 , _ := io .ReadAll (resp3 .Body )
1345+ _ = resp3 .Body .Close ()
1346+
1347+ body3Str := string (body3 )
1348+ t .Logf ("Client C get session: %s" , body3Str )
1349+ assert .Contains (t , body3Str , "SESSION_READ" )
1350+ assert .Contains (t , body3Str , "secret=NOT_FOUND" ,
1351+ "Client C should not find any secret.\n Response: %s" , body3Str )
1352+ assert .Contains (t , body3Str , "client_id=NOT_FOUND" ,
1353+ "Client C should not find any client_id.\n Response: %s" , body3Str )
1354+
1355+ }, & testOptions {
1356+ workerScript : "session-leak.php" ,
1357+ nbWorkers : 1 ,
1358+ nbParallelRequests : 1 ,
1359+ realServer : true ,
1360+ })
1361+ }
1362+
1363+ func TestSessionNoLeakAfterExit_worker (t * testing.T ) {
1364+ runTest (t , func (_ func (http.ResponseWriter , * http.Request ), ts * httptest.Server , i int ) {
1365+ // Client A: Set a secret value in session and call exit(1)
1366+ clientA := & http.Client {}
1367+ resp1 , err := clientA .Get (ts .URL + "/session-leak.php?action=set_and_exit&value=exit_secret&client_id=exitClient" )
1368+ assert .NoError (t , err )
1369+ body1 , _ := io .ReadAll (resp1 .Body )
1370+ _ = resp1 .Body .Close ()
1371+
1372+ body1Str := string (body1 )
1373+ t .Logf ("Client A set and exit: %s" , body1Str )
1374+ // The response may be incomplete due to exit(1)
1375+ assert .Contains (t , body1Str , "BEFORE_EXIT" )
1376+
1377+ // Wait a bit for the worker to restart after exit(1)
1378+ time .Sleep (100 * time .Millisecond )
1379+
1380+ // Client B: Check that session is empty (should not see Client A's data)
1381+ clientB := & http.Client {}
1382+ resp2 , err := clientB .Get (ts .URL + "/session-leak.php?action=check_empty" )
1383+ assert .NoError (t , err )
1384+ body2 , _ := io .ReadAll (resp2 .Body )
1385+ _ = resp2 .Body .Close ()
1386+
1387+ body2Str := string (body2 )
1388+ t .Logf ("Client B check empty after exit: %s" , body2Str )
1389+ assert .Contains (t , body2Str , "SESSION_CHECK" )
1390+ assert .Contains (t , body2Str , "SESSION_EMPTY=true" ,
1391+ "Client B should have empty session after Client A's exit(1).\n Response: %s" , body2Str )
1392+ assert .NotContains (t , body2Str , "exit_secret" ,
1393+ "Client A's secret should not leak to Client B after exit(1).\n Response: %s" , body2Str )
1394+
1395+ // Client C: Try to read session (should also be empty)
1396+ clientC := & http.Client {}
1397+ resp3 , err := clientC .Get (ts .URL + "/session-leak.php?action=get" )
1398+ assert .NoError (t , err )
1399+ body3 , _ := io .ReadAll (resp3 .Body )
1400+ _ = resp3 .Body .Close ()
1401+
1402+ body3Str := string (body3 )
1403+ t .Logf ("Client C get session after exit: %s" , body3Str )
1404+ assert .Contains (t , body3Str , "SESSION_READ" )
1405+ assert .Contains (t , body3Str , "secret=NOT_FOUND" ,
1406+ "Client C should not find any secret after exit(1).\n Response: %s" , body3Str )
1407+
1408+ }, & testOptions {
1409+ workerScript : "session-leak.php" ,
1410+ nbWorkers : 1 ,
1411+ nbParallelRequests : 1 ,
1412+ realServer : true ,
1413+ })
1414+ }
0 commit comments