@@ -847,7 +847,7 @@ public function testGetUnauthenticatedRedirectUrlWithBasePath()
847847 );
848848 }
849849
850- public function testGetLoginRedirect ()
850+ public function testGetLoginRedirectInvalid ()
851851 {
852852 $ service = new AuthenticationService ([
853853 'unauthenticatedRedirect ' => '/users/login ' ,
@@ -888,6 +888,199 @@ public function testGetLoginRedirect()
888888 '/path/with?query=string ' ,
889889 $ service ->getLoginRedirect ($ request )
890890 );
891+
892+ $ request = ServerRequestFactory::fromGlobals (
893+ ['REQUEST_URI ' => '/login ' ],
894+ ['redirect ' => '/ \\evil.com ' ]
895+ );
896+ $ this ->assertNull (
897+ $ service ->getLoginRedirect ($ request )
898+ );
899+
900+ $ request = ServerRequestFactory::fromGlobals (
901+ ['REQUEST_URI ' => '/login ' ],
902+ ['redirect ' => '\\/ \\evil.com ' ]
903+ );
904+ $ this ->assertNull (
905+ $ service ->getLoginRedirect ($ request )
906+ );
907+
908+ $ request = ServerRequestFactory::fromGlobals (
909+ ['REQUEST_URI ' => '/login ' ],
910+ ['redirect ' => '\\evil.com/path ' ]
911+ );
912+ $ this ->assertSame (
913+ '/evil.com/path ' ,
914+ $ service ->getLoginRedirect ($ request )
915+ );
916+
917+ $ request = ServerRequestFactory::fromGlobals (
918+ ['REQUEST_URI ' => '/login ' ],
919+ ['redirect ' => '\\\\evil.com/path ' ]
920+ );
921+ $ this ->assertNull (
922+ $ service ->getLoginRedirect ($ request )
923+ );
924+ }
925+
926+ /**
927+ * testGetLoginRedirectValidationDisabled
928+ *
929+ * @return void
930+ */
931+ public function testGetLoginRedirectValidationDisabled ()
932+ {
933+ $ service = new AuthenticationService ([
934+ 'unauthenticatedRedirect ' => '/users/login ' ,
935+ 'queryParam ' => 'redirect ' ,
936+ 'redirectValidation ' => [
937+ 'enabled ' => false ,
938+ ]
939+ ]);
940+
941+ // With validation disabled, even nested redirects should pass
942+ $ request = ServerRequestFactory::fromGlobals (
943+ ['REQUEST_URI ' => '/secrets ' ],
944+ ['redirect ' => '/login?redirect=/secrets ' ]
945+ );
946+ $ this ->assertSame (
947+ '/login?redirect=/secrets ' ,
948+ $ service ->getLoginRedirect ($ request )
949+ );
950+ }
951+
952+ /**
953+ * testGetLoginRedirectValidationNestedRedirects
954+ *
955+ * @return void
956+ */
957+ public function testGetLoginRedirectValidationNestedRedirects ()
958+ {
959+ $ service = new AuthenticationService ([
960+ 'unauthenticatedRedirect ' => '/users/login ' ,
961+ 'queryParam ' => 'redirect ' ,
962+ 'redirectValidation ' => [
963+ 'enabled ' => true ,
964+ ]
965+ ]);
966+
967+ // Valid single-level redirect
968+ $ request = ServerRequestFactory::fromGlobals (
969+ ['REQUEST_URI ' => '/secrets ' ],
970+ ['redirect ' => '/articles/view/1 ' ]
971+ );
972+ $ this ->assertSame (
973+ '/articles/view/1 ' ,
974+ $ service ->getLoginRedirect ($ request )
975+ );
976+
977+ // Nested redirect (should be blocked)
978+ $ request = ServerRequestFactory::fromGlobals (
979+ ['REQUEST_URI ' => '/secrets ' ],
980+ ['redirect ' => '/login?redirect=/articles/view/1 ' ]
981+ );
982+ $ this ->assertNull ($ service ->getLoginRedirect ($ request ));
983+
984+ // Deeply nested redirect (should be blocked)
985+ $ request = ServerRequestFactory::fromGlobals (
986+ ['REQUEST_URI ' => '/secrets ' ],
987+ ['redirect ' => '/login?redirect=%2Flogin%3Fredirect%3D%252Farticles ' ]
988+ );
989+ $ this ->assertNull ($ service ->getLoginRedirect ($ request ));
990+ }
991+
992+ /**
993+ * testGetLoginRedirectValidationEncodingLevels
994+ *
995+ * @return void
996+ */
997+ public function testGetLoginRedirectValidationEncodingLevels ()
998+ {
999+ $ service = new AuthenticationService ([
1000+ 'unauthenticatedRedirect ' => '/users/login ' ,
1001+ 'queryParam ' => 'redirect ' ,
1002+ 'redirectValidation ' => [
1003+ 'enabled ' => true ,
1004+ ]
1005+ ]);
1006+
1007+ // Normal single-encoded URL (should pass)
1008+ $ request = ServerRequestFactory::fromGlobals (
1009+ ['REQUEST_URI ' => '/secrets ' ],
1010+ ['redirect ' => '/articles%2Fview%2F1 ' ],
1011+ );
1012+ $ this ->assertSame (
1013+ '/articles%2Fview%2F1 ' ,
1014+ $ service ->getLoginRedirect ($ request )
1015+ );
1016+
1017+ // Double-encoded URL (should be blocked)
1018+ $ request = ServerRequestFactory::fromGlobals (
1019+ ['REQUEST_URI ' => '/secrets ' ],
1020+ ['redirect ' => '/articles%252Fview%252F1 ' ]
1021+ );
1022+ $ this ->assertNull ($ service ->getLoginRedirect ($ request ));
1023+ }
1024+
1025+ /**
1026+ * testGetLoginRedirectValidationMaxLength
1027+ *
1028+ * @return void
1029+ */
1030+ public function testGetLoginRedirectValidationMaxLength ()
1031+ {
1032+ $ service = new AuthenticationService ([
1033+ 'unauthenticatedRedirect ' => '/users/login ' ,
1034+ 'queryParam ' => 'redirect ' ,
1035+ 'redirectValidation ' => [
1036+ 'enabled ' => true ,
1037+ 'maxLength ' => 100 ,
1038+ ]
1039+ ]);
1040+
1041+ // Short URL (should pass)
1042+ $ request = ServerRequestFactory::fromGlobals (
1043+ ['REQUEST_URI ' => '/secrets ' ],
1044+ ['redirect ' => '/articles/view/1 ' ]
1045+ );
1046+ $ this ->assertSame (
1047+ '/articles/view/1 ' ,
1048+ $ service ->getLoginRedirect ($ request )
1049+ );
1050+
1051+ // Excessively long URL (should be blocked)
1052+ $ longUrl = '/articles/ ' . str_repeat ('a ' , 150 );
1053+ $ request = ServerRequestFactory::fromGlobals (
1054+ ['REQUEST_URI ' => '/secrets ' ],
1055+ ['redirect ' => $ longUrl ]
1056+ );
1057+ $ this ->assertNull ($ service ->getLoginRedirect ($ request ));
1058+ }
1059+
1060+ /**
1061+ * testGetLoginRedirectValidationWithQueryParameters
1062+ *
1063+ * @return void
1064+ */
1065+ public function testGetLoginRedirectValidationWithQueryParameters ()
1066+ {
1067+ $ service = new AuthenticationService ([
1068+ 'unauthenticatedRedirect ' => '/users/login ' ,
1069+ 'queryParam ' => 'redirect ' ,
1070+ 'redirectValidation ' => [
1071+ 'enabled ' => true ,
1072+ ]
1073+ ]);
1074+
1075+ // Valid redirect with query parameters
1076+ $ request = ServerRequestFactory::fromGlobals (
1077+ ['REQUEST_URI ' => '/secrets ' ],
1078+ ['redirect ' => '/articles/index?sort=created&direction=desc ' ]
1079+ );
1080+ $ this ->assertSame (
1081+ '/articles/index?sort=created&direction=desc ' ,
1082+ $ service ->getLoginRedirect ($ request )
1083+ );
8911084 }
8921085
8931086 /**
0 commit comments