Security: prevent path traversal in character/user/file save and delete

This commit is contained in:
oobabooga 2026-03-06 01:59:18 -03:00
parent 521ddbb722
commit eba262d47a
3 changed files with 22 additions and 3 deletions

View file

@ -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}')

View file

@ -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()

View file

@ -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()