@@ -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,111 @@ 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+ // Client B: Check that session is empty (should not see Client A's data)
1378+ // Retry until the worker has restarted after exit(1)
1379+ clientB := & http.Client {}
1380+ var body2Str string
1381+ assert .Eventually (t , func () bool {
1382+ resp2 , err := clientB .Get (ts .URL + "/session-leak.php?action=check_empty" )
1383+ if err != nil {
1384+ return false
1385+ }
1386+ body2 , _ := io .ReadAll (resp2 .Body )
1387+ _ = resp2 .Body .Close ()
1388+ body2Str = string (body2 )
1389+ return strings .Contains (body2Str , "SESSION_CHECK" )
1390+ }, 2 * time .Second , 10 * time .Millisecond , "Worker did not restart in time after exit(1)" )
1391+
1392+ t .Logf ("Client B check empty after exit: %s" , body2Str )
1393+ assert .Contains (t , body2Str , "SESSION_EMPTY=true" ,
1394+ "Client B should have empty session after Client A's exit(1).\n Response: %s" , body2Str )
1395+ assert .NotContains (t , body2Str , "exit_secret" ,
1396+ "Client A's secret should not leak to Client B after exit(1).\n Response: %s" , body2Str )
1397+
1398+ // Client C: Try to read session (should also be empty)
1399+ clientC := & http.Client {}
1400+ resp3 , err := clientC .Get (ts .URL + "/session-leak.php?action=get" )
1401+ assert .NoError (t , err )
1402+ body3 , _ := io .ReadAll (resp3 .Body )
1403+ _ = resp3 .Body .Close ()
1404+
1405+ body3Str := string (body3 )
1406+ t .Logf ("Client C get session after exit: %s" , body3Str )
1407+ assert .Contains (t , body3Str , "SESSION_READ" )
1408+ assert .Contains (t , body3Str , "secret=NOT_FOUND" ,
1409+ "Client C should not find any secret after exit(1).\n Response: %s" , body3Str )
1410+
1411+ }, & testOptions {
1412+ workerScript : "session-leak.php" ,
1413+ nbWorkers : 1 ,
1414+ nbParallelRequests : 1 ,
1415+ realServer : true ,
1416+ })
1417+ }
0 commit comments