@@ -1014,15 +1014,37 @@ public function restore_documents($session_id = 0, $respect_base_content = false
10141014 return $ this ->resourceFileAbsPathFromDocument ($ srcDoc ) ?: null ;
10151015 }
10161016
1017- $ p = $ srcRoot .(string ) $ item ->path ;
1018- if (is_file ($ p ) && is_readable ($ p )) {
1017+ // Defense in depth against path traversal: $item->path comes from the
1018+ // deserialized backup metadata, so an authenticated user importing a tampered
1019+ // backup fully controls it. Only accept files that resolve *inside* the
1020+ // extraction root, so a forged path such as "../../../../etc/passwd" cannot
1021+ // read files outside the backup directory.
1022+ $ containedRealPath = static function (string $ root , string $ relPath ): ?string {
1023+ $ rootReal = realpath (rtrim ($ root , '/ ' ));
1024+ if (false === $ rootReal ) {
1025+ return null ;
1026+ }
1027+ $ candidate = realpath ($ root .$ relPath );
1028+ if (false === $ candidate || !is_file ($ candidate ) || !is_readable ($ candidate )) {
1029+ return null ;
1030+ }
1031+ $ rootReal = rtrim ($ rootReal , '/ ' ).'/ ' ;
1032+ if (!str_starts_with ($ candidate , $ rootReal )) {
1033+ return null ;
1034+ }
1035+
1036+ return $ candidate ;
1037+ };
1038+
1039+ $ p = $ containedRealPath ((string ) $ srcRoot , (string ) $ item ->path );
1040+ if (null !== $ p ) {
10191041 return $ p ;
10201042 }
10211043
10221044 $ altRoot = rtrim ((string ) ($ this ->course ->resources ['__meta ' ]['archiver_root ' ] ?? '' ), '/ ' ).'/ ' ;
10231045 if ($ altRoot && $ altRoot !== $ srcRoot ) {
1024- $ p2 = $ altRoot. (string ) $ item ->path ;
1025- if (is_file ( $ p2 ) && is_readable ( $ p2) ) {
1046+ $ p2 = $ containedRealPath ( $ altRoot, (string ) $ item ->path ) ;
1047+ if (null !== $ p2 ) {
10261048 return $ p2 ;
10271049 }
10281050 }
0 commit comments