MindsDB has Path Traversal in /api/files Leading to Remote Code Execution
Description
MindsDB is a platform for building artificial intelligence from enterprise data. Prior to version 25.9.1.1, there is a path traversal vulnerability in Mindsdb's /api/files interface, which an authenticated attacker can exploit to achieve remote command execution. The vulnerability exists in the "Upload File" module, which corresponds to the API endpoint /api/files. Since the multipart file upload does not perform security checks on the uploaded file path, an attacker can perform path traversal by using ../ sequences in the filename field. The file write operation occurs before calling clear_filename and save_file, meaning there is no filtering of filenames or file types, allowing arbitrary content to be written to any path on the server. Version 25.9.1.1 patches the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
mindsdbPyPI | < 25.9.1.1 | 25.9.1.1 |
Affected products
1Patches
187a44bdb2b97Better handling of uploaded file names (#11507)
3 files changed · +87 −91
mindsdb/api/http/namespaces/file.py+9 −3 modified@@ -3,6 +3,7 @@ import tarfile import tempfile import zipfile +from pathlib import Path from urllib.parse import urlparse import multipart @@ -60,7 +61,10 @@ def on_field(field): def on_file(file): nonlocal file_object - data["file"] = file.file_name.decode() + file_name = file.file_name.decode() + data["file"] = file_name + if Path(file_name).name != file_name: + raise ValueError(f"Wrong file name: {file_name}") file_object = file.file_object temp_dir_path = tempfile.mkdtemp(prefix="mindsdb_file_") @@ -72,8 +76,9 @@ def on_file(file): on_file=on_file, config={ "UPLOAD_DIR": temp_dir_path.encode(), # bytes required - "UPLOAD_KEEP_FILENAME": True, + "UPLOAD_KEEP_FILENAME": False, "UPLOAD_KEEP_EXTENSIONS": True, + "UPLOAD_DELETE_TMP": False, "MAX_MEMORY_FILE_SIZE": 0, }, ) @@ -93,6 +98,7 @@ def on_file(file): except (AttributeError, ValueError, OSError): logger.debug("Failed to flush file_object before closing.", exc_info=True) file_object.close() + Path(file_object.name).rename(Path(file_object.name).parent / data["file"]) file_object = None else: data = request.json @@ -101,7 +107,7 @@ def on_file(file): return http_error( 400, "File already exists", - f"File with name '{data['file']}' already exists", + f"File with name '{mindsdb_file_name}' already exists", ) if data.get("source_type") == "url":
mindsdb/api/http/namespaces/handlers.py+77 −87 modified@@ -19,96 +19,94 @@ from mindsdb.api.http.namespaces.configs.handlers import ns_conf from mindsdb.api.executor.controllers.session_controller import SessionController from mindsdb.api.executor.command_executor import ExecuteCommands -from mindsdb.utilities.config import Config -@ns_conf.route('/') +@ns_conf.route("/") class HandlersList(Resource): - @ns_conf.doc('handlers_list') - @api_endpoint_metrics('GET', '/handlers') + @ns_conf.doc("handlers_list") + @api_endpoint_metrics("GET", "/handlers") def get(self): - '''List all db handlers''' + """List all db handlers""" - if request.args.get('lazy') == '1': + if request.args.get("lazy") == "1": handlers = ca.integration_controller.get_handlers_metadata() else: handlers = ca.integration_controller.get_handlers_import_status() result = [] for handler_type, handler_meta in handlers.items(): # remove non-integration handlers - if handler_type not in ['utilities', 'dummy_data']: - row = {'name': handler_type} + if handler_type not in ["utilities", "dummy_data"]: + row = {"name": handler_type} row.update(handler_meta) - del row['path'] + del row["path"] result.append(row) return result -@ns_conf.route('/<handler_name>/icon') +@ns_conf.route("/<handler_name>/icon") class HandlerIcon(Resource): - @ns_conf.param('handler_name', 'Handler name') - @api_endpoint_metrics('GET', '/handlers/handler/icon') + @ns_conf.param("handler_name", "Handler name") + @api_endpoint_metrics("GET", "/handlers/handler/icon") def get(self, handler_name): try: handler_meta = ca.integration_controller.get_handlers_metadata().get(handler_name) if handler_meta is None: - return http_error(HTTPStatus.NOT_FOUND, 'Icon not found', f'Icon for {handler_name} not found') - icon_name = handler_meta['icon']['name'] - handler_folder = handler_meta['import']['folder'] - mindsdb_path = Path(importlib.util.find_spec('mindsdb').origin).parent - icon_path = mindsdb_path.joinpath('integrations/handlers').joinpath(handler_folder).joinpath(icon_name) + return http_error(HTTPStatus.NOT_FOUND, "Icon not found", f"Icon for {handler_name} not found") + icon_name = handler_meta["icon"]["name"] + handler_folder = handler_meta["import"]["folder"] + mindsdb_path = Path(importlib.util.find_spec("mindsdb").origin).parent + icon_path = mindsdb_path.joinpath("integrations/handlers").joinpath(handler_folder).joinpath(icon_name) if icon_path.is_absolute() is False: icon_path = Path(os.getcwd()).joinpath(icon_path) except Exception: - return http_error(HTTPStatus.NOT_FOUND, 'Icon not found', f'Icon for {handler_name} not found') + return http_error(HTTPStatus.NOT_FOUND, "Icon not found", f"Icon for {handler_name} not found") else: return send_file(icon_path) -@ns_conf.route('/<handler_name>') +@ns_conf.route("/<handler_name>") class HandlerInfo(Resource): - @ns_conf.param('handler_name', 'Handler name') - @api_endpoint_metrics('GET', '/handlers/handler') + @ns_conf.param("handler_name", "Handler name") + @api_endpoint_metrics("GET", "/handlers/handler") def get(self, handler_name): - handler_meta = ca.integration_controller.get_handler_meta(handler_name) - row = {'name': handler_name} + row = {"name": handler_name} row.update(handler_meta) - del row['path'] - del row['icon'] + del row["path"] + del row["icon"] return row -@ns_conf.route('/<handler_name>/install') +@ns_conf.route("/<handler_name>/install") class InstallDependencies(Resource): - @ns_conf.param('handler_name', 'Handler name') - @api_endpoint_metrics('POST', '/handlers/handler/install') + @ns_conf.param("handler_name", "Handler name") + @api_endpoint_metrics("POST", "/handlers/handler/install") def post(self, handler_name): handler_meta = ca.integration_controller.get_handler_meta(handler_name) if handler_meta is None: - return f'Unknown handler: {handler_name}', 400 + return f"Unknown handler: {handler_name}", 400 - if handler_meta.get('import', {}).get('success', False) is True: - return 'Installed', 200 + if handler_meta.get("import", {}).get("success", False) is True: + return "Installed", 200 - dependencies = handler_meta['import']['dependencies'] + dependencies = handler_meta["import"]["dependencies"] if len(dependencies) == 0: - return 'Installed', 200 + return "Installed", 200 result = install_dependencies(dependencies) # reload it if any result, so we can get new error message ca.integration_controller.reload_handler_module(handler_name) - if result.get('success') is True: + if result.get("success") is True: # If warm processes are available in the cache, remove them. # This will force a new process to be created with the installed dependencies. process_cache.remove_processes_for_handler(handler_name) - return '', 200 + return "", 200 return http_error( 500, - f'Failed to install dependencies for {handler_meta.get("title", handler_name)}', - result.get('error_message', 'unknown error') + f"Failed to install dependencies for {handler_meta.get('title', handler_name)}", + result.get("error_message", "unknown error"), ) @@ -122,24 +120,24 @@ def on_field(field): params[name] = value def on_file(file): - params[file.field_name.decode()] = file.file_object - file_names.append(file.field_name.decode()) + file_name = file.field_name.decode() + if file_name not in ("code", "modules"): + raise ValueError(f"Wrong field name: {file_name}") + params[file_name] = file.file_object + file_names.append(file_name) - temp_dir_path = temp_dir_path = tempfile.mkdtemp( - prefix='mindsdb_byom_file_', - dir=Config().paths['tmp'] - ) + temp_dir_path = tempfile.mkdtemp(prefix="mindsdb_file_") parser = multipart.create_form_parser( headers=request.headers, on_field=on_field, on_file=on_file, config={ - 'UPLOAD_DIR': temp_dir_path.encode(), # bytes required - 'UPLOAD_KEEP_FILENAME': True, - 'UPLOAD_KEEP_EXTENSIONS': True, - 'MAX_MEMORY_FILE_SIZE': 0 - } + "UPLOAD_DIR": temp_dir_path.encode(), # bytes required + "UPLOAD_KEEP_FILENAME": True, + "UPLOAD_KEEP_EXTENSIONS": True, + "MAX_MEMORY_FILE_SIZE": float("inf"), + }, ) while True: @@ -151,32 +149,33 @@ def on_file(file): parser.close() for file_name in file_names: + file_path = os.path.join(temp_dir_path, file_name) + with open(file_path, "wb") as f: + params[file_name].seek(0) + f.write(params[file_name].read()) params[file_name].close() + params[file_name] = file_path return params -@ns_conf.route('/byom/<name>') -@ns_conf.param('name', "Name of the model") +@ns_conf.route("/byom/<name>") +@ns_conf.param("name", "Name of the model") class BYOMUpload(Resource): - @ns_conf.doc('post_file') - @api_endpoint_metrics('POST', '/handlers/byom/handler') + @ns_conf.doc("post_file") + @api_endpoint_metrics("POST", "/handlers/byom/handler") def post(self, name): params = prepare_formdata() - code_file_path = params['code'].name.decode() + code_file_path = params["code"] try: - module_file_path = params['modules'].name.decode() + module_file_path = params["modules"] except AttributeError: - module_file_path = Path(code_file_path).parent / 'requirements.txt' + module_file_path = Path(code_file_path).parent / "requirements.txt" module_file_path.touch() module_file_path = str(module_file_path) - connection_args = { - 'code': code_file_path, - 'modules': module_file_path, - 'type': params.get('type') - } + connection_args = {"code": code_file_path, "modules": module_file_path, "type": params.get("type")} session = SessionController() @@ -185,48 +184,39 @@ def post(self, name): engine_storage = HandlerStorage(base_ml_handler.integration_id) - engine_versions = [ - int(x) for x in engine_storage.get_connection_args()['versions'].keys() - ] + engine_versions = [int(x) for x in engine_storage.get_connection_args()["versions"].keys()] - return { - 'last_engine_version': max(engine_versions), - 'engine_versions': engine_versions - } + return {"last_engine_version": max(engine_versions), "engine_versions": engine_versions} - @ns_conf.doc('put_file') - @api_endpoint_metrics('PUT', '/handlers/byom/handler') + @ns_conf.doc("put_file") + @api_endpoint_metrics("PUT", "/handlers/byom/handler") def put(self, name): - ''' upload new model - params in FormData: - - code - - modules - ''' + """upload new model + params in FormData: + - code + - modules + """ params = prepare_formdata() - code_file_path = params['code'].name.decode() + code_file_path = params["code"] try: - module_file_path = params['modules'].name.decode() + module_file_path = params["modules"] except KeyError: - module_file_path = Path(code_file_path).parent / 'requirements.txt' + module_file_path = Path(code_file_path).parent / "requirements.txt" module_file_path.touch() module_file_path = str(module_file_path) connection_args = { - 'code': code_file_path, - 'modules': module_file_path, - 'mode': params.get('mode'), - 'type': params.get('type') + "code": code_file_path, + "modules": module_file_path, + "mode": params.get("mode"), + "type": params.get("type"), } - ast_query = CreateMLEngine( - name=Identifier(name), - handler='byom', - params=connection_args - ) + ast_query = CreateMLEngine(name=Identifier(name), handler="byom", params=connection_args) sql_session = SessionController() command_executor = ExecuteCommands(sql_session) command_executor.execute_command(ast_query) - return '', 200 + return "", 200
requirements/requirements.txt+1 −1 modified@@ -3,7 +3,7 @@ flask == 3.0.3 werkzeug == 3.0.6 flask-restx >= 1.3.0, < 2.0.0 pandas == 2.2.3 -python-multipart == 0.0.18 +python-multipart == 0.0.20 cryptography>=35.0 psycopg[binary] psutil~=7.0
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-4894-xqv6-vrfqghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-27483ghsaADVISORY
- github.com/mindsdb/mindsdb/commit/87a44bdb2b97f963e18f10a068e1a1e2690505efghsax_refsource_MISCWEB
- github.com/mindsdb/mindsdb/releases/tag/v25.9.1.1ghsax_refsource_MISCWEB
- github.com/mindsdb/mindsdb/security/advisories/GHSA-4894-xqv6-vrfqghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.