CVE-2022-2872 (published 2022-09-21T10:15Z)

Unrestricted Upload of File with Dangerous Type in GitHub repository octoprint/octoprint prior to 1.8.3.

CVSS v3 base score: 5.4, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N)

Weakness information:
cwe_id: CWE-434
cwe_name: Unrestricted Upload of File with Dangerous Type
description: The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.
extended_description: 
            
            
            
            
            
         
         

Fix information:
repo_url: https://github.com/octoprint/octoprint
commit_details: repo_url: https://github.com/octoprint/octoprint
author: Gina Häußge
author_date: 2022-08-17 00:45:31+02:00
msg: 🔒️ Enforce valid type on copy/move of uploads

Also enforce that on downloads and mirror it on the
API.

Prevents accidental or intentional renaming of
valid files to something like .html to e.g. attempt
to run arbitrary JS code in the browser on
"download" in the context of the OctoPrint URL.
Stealing login credentials via XSS is very unlikely
here given their http-only nature, however it's
still better to prevent this from happening anyhow.
repository: repo_url: https://github.com/octoprint/octoprint
repo_name: OctoPrint/OctoPrint
description: OctoPrint is the snappy web interface for your 3D printer!
repo_language: Python
owner: octoprint
file_changes: filename: storage.py
change_type: ModificationType.MODIFY
diff: @@ -954,6 +954,12 @@ class LocalFileStorage(StorageInterface):
             source, destination, must_not_equal=True
         )
 
+        if not octoprint.filemanager.valid_file_type(destination_data["name"]):
+            raise StorageError(
+                f"{destination_data['name']} is an unrecognized file type",
+                code=StorageError.INVALID_FILE,
+            )
+
         try:
             shutil.copy2(source_data["fullpath"], destination_data["fullpath"])
         except Exception as e:
@@ -983,6 +989,12 @@ class LocalFileStorage(StorageInterface):
             source, destination
         )
 
+        if not octoprint.filemanager.valid_file_type(destination_data["name"]):
+            raise StorageError(
+                f"{destination_data['name']} is an unrecognized file type",
+                code=StorageError.INVALID_FILE,
+            )
+
         # only a display rename? Update that and bail early
         if source_data["fullpath"] == destination_data["fullpath"]:
             self._set_display_metadata(destination_data)

programming_language: Python
filename: __init__.py
change_type: ModificationType.MODIFY
diff: @@ -40,6 +40,7 @@ from watchdog.observers import Observer
 from watchdog.observers.polling import PollingObserver
 from werkzeug.exceptions import HTTPException
 
+import octoprint.filemanager
 import octoprint.util
 import octoprint.util.net
 from octoprint.server import util
@@ -746,6 +747,15 @@ class Server:
             )
         }
 
+        only_known_types_validator = {
+            "path_validation": util.tornado.path_validation_factory(
+                lambda path: octoprint.filemanager.valid_file_type(
+                    os.path.basename(path)
+                ),
+                status_code=404,
+            )
+        }
+
         valid_timelapse = lambda path: not octoprint.util.is_hidden_path(path) and (
             octoprint.timelapse.valid_timelapse(path)
             or octoprint.timelapse.valid_timelapse_thumbnail(path)
@@ -840,6 +850,7 @@ class Server:
                     download_permission_validator,
                     download_handler_kwargs,
                     no_hidden_files_validator,
+                    only_known_types_validator,
                     additional_mime_types,
                 ),
             ),

programming_language: Python
filename: files.py
change_type: ModificationType.MODIFY
diff: @@ -1138,42 +1138,55 @@ def gcodeFileCommand(filename, target):
             if not (is_file or is_folder):
                 abort(400, description=f"Neither file nor folder, can't {command}")
 
-            if command == "copy":
-                # destination already there? error...
-                if _verifyFileExists(target, destination) or _verifyFolderExists(
-                    target, destination
-                ):
-                    abort(409, description="File or folder does already exist")
-
-                if is_file:
-                    fileManager.copy_file(target, filename, destination)
-                else:
-                    fileManager.copy_folder(target, filename, destination)
-
-            elif command == "move":
-                with Permissions.FILES_DELETE.require(403):
-                    if _isBusy(target, filename):
-                        abort(
-                            409,
-                            description="Trying to move a file or folder that is currently in use",
-                        )
-
-                    # destination already there AND not ourselves (= display rename)? error...
-                    if (
-                        _verifyFileExists(target, destination)
-                        or _verifyFolderExists(target, destination)
-                    ) and sanitized_destination != filename:
+            try:
+                if command == "copy":
+                    # destination already there? error...
+                    if _verifyFileExists(target, destination) or _verifyFolderExists(
+                        target, destination
+                    ):
                         abort(409, description="File or folder does already exist")
 
-                    # deselect the file if it's currently selected
-                    currentOrigin, currentFilename = _getCurrentFile()
-                    if currentFilename is not None and filename == currentFilename:
-                        printer.unselect_file()
-
                     if is_file:
-                        fileManager.move_file(target, filename, destination)
+                        fileManager.copy_file(target, filename, destination)
                     else:
-                        fileManager.move_folder(target, filename, destination)
+                        fileManager.copy_folder(target, filename, destination)
+
+                elif command == "move":
+                    with Permissions.FILES_DELETE.require(403):
+                        if _isBusy(target, filename):
+                            abort(
+                                409,
+                                description="Trying to move a file or folder that is currently in use",
+                            )
+
+                        # destination already there AND not ourselves (= display rename)? error...
+                        if (
+                            _verifyFileExists(target, destination)
+                            or _verifyFolderExists(target, destination)
+                        ) and sanitized_destination != filename:
+                            abort(409, description="File or folder does already exist")
+
+                        # deselect the file if it's currently selected
+                        currentOrigin, currentFilename = _getCurrentFile()
+                        if currentFilename is not None and filename == currentFilename:
+                            printer.unselect_file()
+
+                        if is_file:
+                            fileManager.move_file(target, filename, destination)
+                        else:
+                            fileManager.move_folder(target, filename, destination)
+
+            except octoprint.filemanager.storage.StorageError as e:
+                if e.code == octoprint.filemanager.storage.StorageError.INVALID_FILE:
+                    abort(
+                        415,
+                        description=f"Could not {command} {filename} to {destination}, invalid type",
+                    )
+                else:
+                    abort(
+                        500,
+                        description=f"Could not {command} {filename} to {destination}",
+                    )
 
             location = url_for(
                 ".readGcodeFile",

programming_language: Python