CVE-2023-32317 (published 2023-05-26T23:15Z)

Autolab is a course management service that enables auto-graded programming assignments. A Tar slip vulnerability was found in the MOSS cheat checker functionality of Autolab. To exploit this vulnerability an authenticated attacker with instructor permissions needs to upload a specially crafted Tar file. Both "Base File Tar" and "Additional file archive" can be fed with Tar files that contain paths outside their target directories (e.g.,  `../../../../tmp/tarslipped2.sh`). When the MOSS cheat checker is started the files inside of the archives are expanded to the attacker-chosen locations. This issue may lead to arbitrary file write within the scope of the running process.  This issue has been addressed in version 2.11.0. Users are advised to upgrade.

CVSS v3 base score: 7.2, HIGH (CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
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.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/autolab/Autolab
commit_details: repo_url: https://github.com/autolab/Autolab
author: Damian Ho
author_date: 2023-05-17 14:32:33+08:00
msg: Merge pull request from GHSA-h8g5-vhm4-wx6g
repository: repo_url: https://github.com/autolab/Autolab
repo_name: autolab/Autolab
description: Course management service that enables auto-graded programming assignments.
repo_language: Ruby
owner: autolab
file_changes: filename: courses_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,7 @@
 require "archive"
 require "csv"
 require "fileutils"
+require "pathname"
 require "statistics"
 
 class CoursesController < ApplicationController
@@ -1107,15 +1108,18 @@ private
         pathname = Archive.get_entry_name(entry)
         next if Archive.looks_like_directory?(pathname)
 
-        destination = if archive
-                        File.join(extFilesDir,
-                                  pathname)
+        output_dir = if archive
+                       extFilesDir
                       else
-                        File.join(baseFilesDir, pathname)
+                        baseFilesDir
                       end
-        pathname.gsub!(%r{/}, "-")
+        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))
+
         # make sure all subdirectories are there
-        File.open(destination, "wb") do |out|
+        File.open(output_file, "wb") do |out|
           out.write Archive.read_entry_file(entry)
           begin
             out.fsync

programming_language: Ruby
filename: archive.rb
change_type: ModificationType.MODIFY
diff: @@ -232,4 +232,15 @@ module Archive
   def self.looks_like_directory?(pathname)
     pathname.ends_with?("/")
   end
+
+  # Check if a path lies (strictly) within a directory
+  # Takes two Pathname objects
+  # https://gist.github.com/henrik/48e8bb74de9d887770dfb3cc6efaa9b2
+  def self.in_dir?(path, dir)
+    path_parts = path.expand_path.each_filename.to_a
+    dir_parts = dir.expand_path.each_filename.to_a
+    return false if path_parts == dir_parts
+
+    dir_parts.zip(path_parts).all? { |x, y| x == y }
+  end
 end

programming_language: Ruby