Sanitize Uploaded Filenames
Turn untrusted filenames into safe basenames by stripping paths, blocking reserved names, normalizing Unicode, and limiting length.
Code
from __future__ import annotations
import re
import unicodedata
_INVALID = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
_WHITESPACE = re.compile(r"\s+")
_RESERVED = {
"CON",
"PRN",
"AUX",
"NUL",
*(f"COM{index}" for index in range(1, 10)),
*(f"LPT{index}" for index in range(1, 10)),
}
def safe_filename(
name: str,
*,
max_length: int = 120,
fallback: str = "file",
) -> str:
if not isinstance(name, str):
raise TypeError("name must be a string")
if (
not isinstance(max_length, int)
or isinstance(max_length, bool)
or not 8 <= max_length <= 255
):
raise ValueError("max_length must be between 8 and 255")
if not isinstance(fallback, str) or not fallback:
raise ValueError("fallback must be a non-empty string")
normalized = unicodedata.normalize("NFKC", name)
normalized = normalized.replace("\\", "/").rsplit("/", 1)[-1]
normalized = "".join(
character
for character in normalized
if unicodedata.category(character)[0] != "C"
)
base = _INVALID.sub("_", normalized)
base = _WHITESPACE.sub(" ", base).strip(" .")
if not base:
return fallback[:max_length]
stem = base.split(".", 1)[0].strip().upper()
if stem in _RESERVED:
base = f"_{base}"
if len(base) > max_length:
head, dot, suffix = base.rpartition(".")
candidate = (
f".{suffix}"
if dot
and 0 < len(suffix) <= 16
and suffix.isalnum()
else ""
)
keep = max(1, max_length - len(candidate))
base = f"{head[:keep].rstrip(' .')}{candidate}"
return base or fallback[:max_length]
def main() -> None:
samples = [
"../../etc/passwd",
"C:\\Windows\\System32\\cmd.exe",
"CON.txt",
" ..hidden.. ",
"résumé 2026.pdf",
"a" * 200 + ".tar.gz",
"report\x00final.pdf",
]
for sample in samples:
print(repr(sample), "->", safe_filename(sample))
try:
safe_filename("ok.txt", max_length=2)
except ValueError as error:
print(error)
if __name__ == "__main__":
main()Report a problem with this exampleOpens a pre-filled issue on GitHub.