CVE-2023-30620: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: mindsdb is a Machine Learning platform to help developers build AI solutions. In affected versions an unsafe extraction is being performed using `tarfile.extractall()` from a remotely retrieved tarball. Which may lead to the writing of the extracted files to an unintended location. Sometimes, the vulnerability is called a TarSlip or a ZipSlip variant. An attacker may leverage this vulnerability to overwrite any local file which the server process has access to. There is no risk of file exposure with this vulnerability. This issue has been addressed in release `23.2.1.0 `. Users are advised to upgrade. There are no known workarounds for this vulnerability.
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.5
Published: 2023-04-21T21:15Z
Repository: https://github.com/mindsdb/mindsdb
Language: Python
Files changed: mindsdb/api/http/namespaces/file.py, mindsdb/api/http/utils.py
Commit date: 2023-02-16 20:31:12 +0100
Commit message: Use safe extract to prevent CVE-2007-4559 bug

Diff: diff --git a/mindsdb/api/http/namespaces/file.py b/mindsdb/api/http/namespaces/file.py
index 91948f95f..adb7316e1 100644
--- a/mindsdb/api/http/namespaces/file.py
+++ b/mindsdb/api/http/namespaces/file.py
@@ -7,11 +7,11 @@ from flask_restx import Resource
 import tempfile
 import multipart
 import requests
 
 from mindsdb.utilities import log
-from mindsdb.api.http.utils import http_error
+from mindsdb.api.http.utils import http_error, safe_extract
 from mindsdb.api.http.namespaces.configs.files import ns_conf
 from mindsdb.utilities.config import Config
 from mindsdb.utilities.context import context as ctx
 
 
@@ -132,11 +132,11 @@ class File(Resource):
             if lp.endswith('.zip'):
                 with zipfile.ZipFile(file_path) as f:
                     f.extractall(temp_dir_path)
             elif lp.endswith('.tar.gz'):
                 with tarfile.open(file_path) as f:
-                    f.extractall(temp_dir_path)
+                    safe_extract(f, temp_dir_path)
             os.remove(file_path)
             files = os.listdir(temp_dir_path)
             if len(files) != 1:
                 os.rmdir(temp_dir_path)
                 return http_error(400, 'Wrong content.', 'Archive must contain only one data file.')
diff --git a/mindsdb/api/http/utils.py b/mindsdb/api/http/utils.py
index 72eb94669..00582cdbd 100644
--- a/mindsdb/api/http/utils.py
+++ b/mindsdb/api/http/utils.py
@@ -1,6 +1,7 @@
 import json
+import os
 
 from flask import Response
 
 
 def http_error(status_code, title, detail=''):
@@ -20,5 +21,19 @@ def http_error(status_code, title, detail=''):
         status=status_code,
         headers={
             'Content-Type': 'application/problem+json'
         }
     )
+
+def __is_within_directory(directory, target):
+    abs_directory = os.path.abspath(directory)
+    abs_target = os.path.abspath(target)
+    prefix = os.path.commonprefix([abs_directory, abs_target])
+    return prefix == abs_directory
+
+def safe_extract(tar, path=".", members=None, *, numeric_owner=False):
+    for member in tar.getmembers():
+        member_path = os.path.join(path, member.name)
+        if not __is_within_directory(path, member_path):
+            raise Exception("Attempted Path Traversal in Tar File")
+    tar.extractall(path, members, numeric_owner) 
+

Vulnerable code: from mindsdb.api.http.utils import http_error
                    f.extractall(temp_dir_path)
Fixed code: from mindsdb.api.http.utils import http_error, safe_extract
                    safe_extract(f, temp_dir_path)
import os

def __is_within_directory(directory, target):
    abs_directory = os.path.abspath(directory)
    abs_target = os.path.abspath(target)
    prefix = os.path.commonprefix([abs_directory, abs_target])
    return prefix == abs_directory

def safe_extract(tar, path=".", members=None, *, numeric_owner=False):
    for member in tar.getmembers():
        member_path = os.path.join(path, member.name)
        if not __is_within_directory(path, member_path):
            raise Exception("Attempted Path Traversal in Tar File")
    tar.extractall(path, members, numeric_owner)