CVE-2023-32676: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Autolab is a course management service that enables auto-graded programming assignments. A Tar slip vulnerability was found in the Install assessment functionality of Autolab. To exploit this vulnerability an authenticated attacker with instructor permissions needs to upload a specially crafted Tar file. Using the install assessment functionality an attacker can feed a Tar file that contain files with paths pointing outside of the target directory (e.g.,  `../../../../tmp/tarslipped1.sh`). When the Install assessment form is submitted the files inside of the archives are expanded to the attacker-chosen locations. This issue has been addressed in version 2.11.0. Users are advised to upgrade.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 7.2
Published: 2023-05-26T23:15Z
Repository: https://github.com/autolab/Autolab
Language: Ruby
Files changed: app/controllers/assessments_controller.rb, app/controllers/courses_controller.rb
Commit date: 2023-05-18 14:23:46 -0700
Commit message: Merge pull request from GHSA-x9hj-r9q4-832c

* use Archive.in_dir? to check entry files in archive stay within archive

* run rubocop

* check that file is specifically within assessment directory

* address PR comment
Diff: diff --git a/app/controllers/assessments_controller.rb b/app/controllers/assessments_controller.rb
index cb17ce1b..881ef76f 100755
--- a/app/controllers/assessments_controller.rb
+++ b/app/controllers/assessments_controller.rb
@@ -185,26 +185,31 @@ class AssessmentsController < ApplicationController
     end
 
     # If all requirements are satisfied, extract assessment files.
     begin
       course_root = Rails.root.join("courses", @course.name)
+      assessment_path = Rails.root.join("courses", @course.name, asmt_name)
       tar_extract.rewind
       tar_extract.each do |entry|
         relative_pathname = entry.full_name
+        entry_file = File.join(course_root, relative_pathname)
+        # Ensure file will lie within course, otherwise skip
+        next unless Archive.in_dir?(Pathname(entry_file), Pathname(assessment_path))
+
         if entry.directory?
-          FileUtils.mkdir_p(File.join(course_root, relative_pathname),
+          FileUtils.mkdir_p(entry_file,
                             mode: entry.header.mode, verbose: false)
         elsif entry.file?
           FileUtils.mkdir_p(File.join(course_root, File.dirname(relative_pathname)),
                             mode: entry.header.mode, verbose: false)
-          File.open(File.join(course_root, relative_pathname), "wb") do |f|
+          File.open(entry_file, "wb") do |f|
             f.write entry.read
           end
-          FileUtils.chmod entry.header.mode, File.join(course_root, relative_pathname),
+          FileUtils.chmod entry.header.mode, entry_file,
                           verbose: false
         elsif entry.header.typeflag == "2"
-          File.symlink entry.header.linkname, File.join(course_root, relative_pathname)
+          File.symlink entry.header.linkname, entry_file
         end
       end
       tar_extract.close
     rescue StandardError => e
       flash[:error] = "Error while extracting tarball to server -- #{e.message}."
diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb
index d4756eae..451ddde9 100755
--- a/app/controllers/courses_controller.rb
+++ b/app/controllers/courses_controller.rb
@@ -1108,13 +1108,13 @@ private
         pathname = Archive.get_entry_name(entry)
         next if Archive.looks_like_directory?(pathname)
 
         output_dir = if archive
                        extFilesDir
-                      else
-                        baseFilesDir
-                      end
+                     else
+                       baseFilesDir
+                     end
         output_file = File.join(output_dir, pathname)
 
         # skip if the file lies outside the archive
         next unless Archive.in_dir?(Pathname(output_file), Pathname(output_dir))
 

Vulnerable code:           FileUtils.mkdir_p(File.join(course_root, relative_pathname),
          File.open(File.join(course_root, relative_pathname), "wb") do |f|
          FileUtils.chmod entry.header.mode, File.join(course_root, relative_pathname),
          File.symlink entry.header.linkname, File.join(course_root, relative_pathname)
                      else
                        baseFilesDir
                      end
Fixed code:       assessment_path = Rails.root.join("courses", @course.name, asmt_name)
        entry_file = File.join(course_root, relative_pathname)
        # Ensure file will lie within course, otherwise skip
        next unless Archive.in_dir?(Pathname(entry_file), Pathname(assessment_path))

          FileUtils.mkdir_p(entry_file,
          File.open(entry_file, "wb") do |f|
          FileUtils.chmod entry.header.mode, entry_file,
          File.symlink entry.header.linkname, entry_file
                     else
                       baseFilesDir
                     end