mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-24 22:02:09 +00:00
38 lines
1.3 KiB
Python
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
|