CVE-2022-42725: Improper Link Resolution Before File Access ('Link Following') (CWE-59)
value: Warpinator through 1.2.14 allows access outside of an intended directory, as demonstrated by symbolic directory links.
CWE description: The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.
CVSS v3 base score: 7.5
Published: 2022-10-10T05:15Z
Repository: https://github.com/linuxmint/warpinator
Language: Python
Files changed: src/transfers.py
Commit date: 2022-09-20 18:57:06 -0400
Commit message: transfers: Use Pathlib.Path instead of GFile for path safety check.

Pathlib.Path will resolve both links and relative paths to their
absolute paths - GFile treated links as real paths, so directory
links could potentially escape the incoming folder.

Follow-up to 5244c33d4c1

Diff: diff --git a/src/transfers.py b/src/transfers.py
index 5075cb5..04e39ed 100644
--- a/src/transfers.py
+++ b/src/transfers.py
@@ -3,10 +3,11 @@
 import os
 import logging
 import stat
 import shutil
 import gettext
+from pathlib import Path
 
 from gi.repository import GLib, Gio, GObject
 
 import util
 from util import FileType
@@ -165,11 +166,11 @@ class FileSender(GObject.Object):
 
 class FileReceiver(GObject.Object):
     def __init__(self, op):
         super(FileReceiver, self).__init__()
         self.save_path = prefs.get_save_path()
-        self.save_path_file = Gio.File.new_for_path(self.save_path)
+        self.save_path_obj = Path(self.save_path).resolve()
         self.op = op
         self.preserve_perms = prefs.preserve_permissions() and util.save_folder_is_native_fs()
         self.preserve_timestamp = prefs.preserve_timestamp() and util.save_folder_is_native_fs()
 
         self.current_path = None
@@ -208,14 +209,16 @@ class FileReceiver(GObject.Object):
             self.current_type = s.file_type
             self.current_mtime = s.time.mtime
             self.current_mtime_usec = s.time.mtime_usec
 
         if not self.current_gfile:
+            # Check for valid path (pathlib.Path resolves both relative and symbolically-linked paths)
+            test_path = Path(path).resolve()
+            if not test_path.is_relative_to(self.save_path_obj):
+                raise Exception(_("Resolved path is not valid: %s -> %s") % (path, str(test_path)))
+
             self.current_gfile = Gio.File.new_for_path(path)
-            # Check for valid path (GFile resolves paths upon creation).
-            if self.save_path_file.get_relative_path(self.current_gfile) is None:
-                raise Exception(_("Resolved path is not valid: %s -> %s") % (path, self.current_gfile.get_path()))
 
         if s.file_type == FileType.DIRECTORY:
             os.makedirs(path, exist_ok=True)
         elif s.file_type == FileType.SYMBOLIC_LINK:
             make_symbolic_link(self.op, path, s.symlink_target)

Vulnerable code:         self.save_path_file = Gio.File.new_for_path(self.save_path)
            # Check for valid path (GFile resolves paths upon creation).
            if self.save_path_file.get_relative_path(self.current_gfile) is None:
                raise Exception(_("Resolved path is not valid: %s -> %s") % (path, self.current_gfile.get_path()))
Fixed code: from pathlib import Path
        self.save_path_obj = Path(self.save_path).resolve()
            # Check for valid path (pathlib.Path resolves both relative and symbolically-linked paths)
            test_path = Path(path).resolve()
            if not test_path.is_relative_to(self.save_path_obj):
                raise Exception(_("Resolved path is not valid: %s -> %s") % (path, str(test_path)))