diff --git a/lib/WeBWorK/ContentGenerator/Instructor/FileManager.pm b/lib/WeBWorK/ContentGenerator/Instructor/FileManager.pm index a1145ec72b..2bf3e685cb 100644 --- a/lib/WeBWorK/ContentGenerator/Instructor/FileManager.pm +++ b/lib/WeBWorK/ContentGenerator/Instructor/FileManager.pm @@ -578,7 +578,15 @@ sub unpack_archive ($c, $archive) { next; } - unless ($tar->extract_file($_)) { + my ($member) = $tar->get_files($_); + if ($member && $member->is_symlink) { + # Secure extract mode refuses links whose targets leave the directory; + # recreate them directly (location already validated above). + unless (symlink($member->linkname, $out_file->to_string)) { + $c->addbadmessage($c->maketext(q{Unable to extract "[_1]": [_2]}, $_, $!)); + next; + } + } elsif (!$tar->extract_file($_)) { $c->addbadmessage($tar->error); next; } diff --git a/lib/WeBWorK/Utils/CourseManagement.pm b/lib/WeBWorK/Utils/CourseManagement.pm index 960e6934b5..5c695658e0 100644 --- a/lib/WeBWorK/Utils/CourseManagement.pm +++ b/lib/WeBWorK/Utils/CourseManagement.pm @@ -972,13 +972,26 @@ sub unarchiveCourse { my $arch = Archive::Tar->new($archivePath); die "The tar file $archivePath is not valid." unless $arch; $arch->setcwd($coursesDir); + + # Secure extract mode refuses symbolic/hard links whose targets leave the + # course directory (CVE-2026-42496/-42497), which the standard template links + # do. Extract the files under secure mode, then recreate the symbolic links. + my @symlinks = grep { $_->is_symlink } $arch->get_files; + $arch->remove(map { $_->full_path } grep { $_->is_symlink || $_->is_hardlink } $arch->get_files); $arch->extract(); if ($arch->error) { + # Remove the partial extraction so move_back can restore a displaced course. + path("$coursesDir/$currCourseID")->remove_tree if -e "$coursesDir/$currCourseID"; _unarchiveCourse_move_back($restoreCourseData); die "Failed to unarchive course directory for course $newCourseID: $arch->error"; } + for my $symlink (@symlinks) { + my $link_path = "$coursesDir/" . $symlink->full_path; + symlink($symlink->linkname, $link_path) unless -e $link_path; + } + ##### step 3: read the course environment for this course ##### my $ce2 = WeBWorK::CourseEnvironment->new({ get_SeedCE($ce), courseName => $currCourseID });