feedBack/lib/safepath.py
2026-06-16 18:47:13 +02:00

38 lines
1.3 KiB
Python

"""Path-containment helper for code that joins attacker-controlled names
under a server-owned root.
"""
from __future__ import annotations
from pathlib import Path
def safe_join(root: Path, name: str) -> Path | None:
"""Resolve ``name`` under ``root`` and return the resolved Path, or
``None`` if it would escape ``root`` or is unrepresentable.
Rejects:
* empty names
* paths that resolve outside ``root`` (``..`` traversal, absolute paths)
* paths the OS can't resolve (embedded NULs, OSError on stat)
Normalizes:
* backslash separators to forward slash so a Windows-style entry
name inside a user-supplied archive can't bypass containment on
POSIX hosts (``..\\foo`` would otherwise be treated as a literal
single filename on Linux and resolve inside ``root`` — but on
Windows the same string IS a traversal; normalising means both
platforms reject it identically).
"""
if not name:
return None
safe = name.replace("\\", "/")
try:
root_resolved = root.resolve()
candidate = (root_resolved / safe).resolve()
if not candidate.is_relative_to(root_resolved):
return None
except (ValueError, OSError):
return None
return candidate