cdnV2

git clone https://github.com/notamitgamer/cdnV2.git
· rebuilt hourlyOpen on GitHub
commit a780d21a926a1f2bf7d0e47680ee5ff5f3d9d616
parent 62321f14a3aade5cb5fbb73a417b753296b9a918
Author: claude-bot <noreply@anthropic.com>
Date:   Mon,  5 Oct 2026 14:19:11 +0000

Accept any filename on upload: sanitize server-side, stop using filename in temp path

- Add sanitize_filename(): drops directory parts, folds accents, replaces unsafe chars with _
- /api/put and /api/put-url use /tmp/<uuid> only (fixes path traversal via filename / Content-Disposition)
- Remove frontend filename rejection and update docs

Diffstat:
Mapp/main.py | 5++---
Mapp/storage.py | 24+++++++++++++++++++++++-
Mapp/templates/documentation.html | 2+-
Mapp/templates/index.html | 7-------
4 files changed, 26 insertions(+), 12 deletions(-)

diff --git a/app/main.py b/app/main.py @@ -280,7 +280,7 @@ async def handle_upload( results = [] for file in files: - temp_path = f"/tmp/{uuid.uuid4()}-{file.filename}" + temp_path = f"/tmp/{uuid.uuid4()}" with open(temp_path, "wb") as f: shutil.copyfileobj(file.file, f) @@ -522,7 +522,7 @@ async def handle_upload_from_url( or f"download-{uuid.uuid4().hex[:8]}" ) - temp_path = f"/tmp/{uuid.uuid4()}-{filename}" + temp_path = f"/tmp/{uuid.uuid4()}" downloaded = 0 async with httpx.AsyncClient( @@ -549,7 +549,6 @@ async def handle_upload_from_url( if "filename=" in cd: filename = cd.split("filename=")[-1].strip('"; ') or filename - temp_path = f"/tmp/{uuid.uuid4()}-{filename}" with open(temp_path, "wb") as f: async for chunk in r.aiter_bytes(): diff --git a/app/storage.py b/app/storage.py @@ -1,6 +1,7 @@ import os import re import time +import unicodedata import uuid import json import tempfile @@ -296,9 +297,30 @@ def _do_upload_bytes(data: bytes, hf_path: str): add=[(data, hf_path)], ) +_UNSAFE_NAME_CHARS = re.compile(r"[^A-Za-z0-9._-]+") + + +def sanitize_filename(filename: str, max_len: int = 100) -> str: + """Turn any user-supplied filename into a safe object-key segment. + + Accepts spaces, accents, symbols, emoji and path fragments: directory parts + are dropped, accents are folded to ASCII, and anything else unsafe becomes + a single underscore. The extension is kept. + """ + name = (filename or "").replace("\\", "/").split("/")[-1] + name = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii") + stem, dot, ext = name.rpartition(".") + if not dot: + stem, ext = name, "" + stem = _UNSAFE_NAME_CHARS.sub("_", stem).strip("._") or "file" + ext = _UNSAFE_NAME_CHARS.sub("", ext)[:10] + stem = stem[: max_len - len(ext) - 1] + return f"{stem}.{ext}" if ext else stem + + async def upload_temp_file(temp_path: str, filename: str, folder: str = "uploads") -> str: slug = str(uuid.uuid4())[:8] - safe_filename = filename.replace(" ", "_") + safe_filename = sanitize_filename(filename) hf_path = f"{folder}/{slug}-{safe_filename}" await asyncio.to_thread(_do_upload, temp_path, hf_path) diff --git a/app/templates/documentation.html b/app/templates/documentation.html @@ -179,7 +179,7 @@ <h3>How it works</h3> <p>Each file is streamed to a local temp path, checked against the rate limiter (50&nbsp;MB/minute, 300&nbsp;MB/hour per client IP), then uploaded to Hugging Face at <code>&lt;folder&gt;/&lt;8-char-uuid&gt;-&lt;original-filename&gt;</code> - (spaces in the filename become underscores). The random prefix means two uploads can never collide, and the + (any characters other than letters, digits, <code>.</code>, <code>_</code> and <code>-</code> become underscores, and accents are simplified). The random prefix means two uploads can never collide, and the filename portion is still readable. If 2 or more files were uploaded together, a <em>batch</em> is also created (see <a href="#batches">Batches</a> below) so the whole set can be shared/downloaded as one link.</p> <h3>Response <code>200</code></h3> diff --git a/app/templates/index.html b/app/templates/index.html @@ -1134,14 +1134,10 @@ const allFiles = Array.from(fileList); const files = []; const rejectedSize = []; - const rejectedName = []; - const allowedNameRegex = /^[a-zA-Z0-9_.\-]+$/; allFiles.forEach(f => { if (f.size > MAX_FILE_SIZE_BYTES) { rejectedSize.push(f.name); - } else if (!allowedNameRegex.test(f.name)) { - rejectedName.push(f.name); } else { files.push(f); } @@ -1151,9 +1147,6 @@ if (rejectedSize.length) { errorMessages.push(`Skipped ${rejectedSize.length} file(s) over ${formatBytes(MAX_FILE_SIZE_BYTES)}: ${rejectedSize.join(', ')}`); } - if (rejectedName.length) { - errorMessages.push(`Skipped ${rejectedName.length} file(s) with invalid characters (only a-z, A-Z, 0-9, _, -, . allowed): ${rejectedName.join(', ')}`); - } if (errorMessages.length) { showError(errorMessages.join(' | '));