From eba262d47ad10c7aa3b7d3d3d35c02e33c45306b Mon Sep 17 00:00:00 2001 From: oobabooga <112222186+oobabooga@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:59:18 -0300 Subject: [PATCH] Security: prevent path traversal in character/user/file save and delete --- modules/chat.py | 9 +++++++-- modules/ui_file_saving.py | 5 ++++- modules/utils.py | 11 +++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/modules/chat.py b/modules/chat.py index 5d7e0e56..bc4fc1d8 100644 --- a/modules/chat.py +++ b/modules/chat.py @@ -36,6 +36,7 @@ from modules.utils import ( delete_file, get_available_characters, get_available_users, + sanitize_filename, save_file ) from modules.web_search import add_web_search_attachments @@ -1557,12 +1558,12 @@ def upload_character(file, img_path, tavern=False): data = yaml.safe_load(decoded_file) if 'char_name' in data: - name = data['char_name'] + name = sanitize_filename(data['char_name']) greeting = data['char_greeting'] context = build_pygmalion_style_context(data) yaml_data = generate_character_yaml(name, greeting, context) else: - name = data['name'] + name = sanitize_filename(data['name']) yaml_data = generate_character_yaml(data['name'], data['greeting'], data['context']) outfile_name = name @@ -1653,6 +1654,7 @@ def generate_instruction_template_yaml(instruction_template): def save_character(name, greeting, context, picture, filename): + filename = sanitize_filename(filename) if filename == "": logger.error("The filename is empty, so the character will not be saved.") return @@ -1668,6 +1670,7 @@ def save_character(name, greeting, context, picture, filename): def delete_character(name, instruct=False): + name = sanitize_filename(name) # Check for character data files for extension in ["yml", "yaml", "json"]: delete_file(shared.user_data_dir / 'characters' / f'{name}.{extension}') @@ -1751,6 +1754,7 @@ def generate_user_yaml(name, user_bio): def save_user(name, user_bio, picture, filename): """Save user profile to YAML file""" + filename = sanitize_filename(filename) if filename == "": logger.error("The filename is empty, so the user will not be saved.") return @@ -1772,6 +1776,7 @@ def save_user(name, user_bio, picture, filename): def delete_user(name): """Delete user profile files""" + name = sanitize_filename(name) # Check for user data files for extension in ["yml", "yaml", "json"]: delete_file(shared.user_data_dir / 'users' / f'{name}.{extension}') diff --git a/modules/ui_file_saving.py b/modules/ui_file_saving.py index 46087ace..3ed256f8 100644 --- a/modules/ui_file_saving.py +++ b/modules/ui_file_saving.py @@ -3,7 +3,7 @@ import traceback import gradio as gr from modules import chat, presets, shared, ui, utils -from modules.utils import gradio +from modules.utils import gradio, sanitize_filename def create_ui(): @@ -91,6 +91,7 @@ def create_event_handlers(): def handle_save_preset_confirm_click(filename, contents): try: + filename = sanitize_filename(filename) utils.save_file(str(shared.user_data_dir / "presets" / f"{filename}.yaml"), contents) available_presets = utils.get_available_presets() output = gr.update(choices=available_presets, value=filename) @@ -106,6 +107,7 @@ def handle_save_preset_confirm_click(filename, contents): def handle_save_confirm_click(root, filename, contents): try: + filename = sanitize_filename(filename) utils.save_file(root + filename, contents) except Exception: traceback.print_exc() @@ -115,6 +117,7 @@ def handle_save_confirm_click(root, filename, contents): def handle_delete_confirm_click(root, filename): try: + filename = sanitize_filename(filename) utils.delete_file(root + filename) except Exception: traceback.print_exc() diff --git a/modules/utils.py b/modules/utils.py index 447685b7..7ab4a554 100644 --- a/modules/utils.py +++ b/modules/utils.py @@ -15,6 +15,17 @@ def gradio(*keys): return [shared.gradio[k] for k in keys] +def sanitize_filename(name): + """Strip path traversal components from a filename. + + Returns only the final path component with leading dots removed, + preventing directory traversal via '../' or absolute paths. + """ + name = Path(name).name # drop all directory components + name = name.lstrip('.') # remove leading dots + return name + + def _is_path_allowed(abs_path_str): """Check if a path is under the project root or the configured user_data directory.""" abs_path = Path(abs_path_str).resolve()