Source code for stegx.safe_paths

from __future__ import annotations

import logging
import os
from typing import Optional

__all__ = [
    "PathValidationError",
    "validate_user_path",
    "ensure_under_base",
    "sink_safe_path",
    "MAX_PATH_LEN",
]

_LOG = logging.getLogger(__name__)


MAX_PATH_LEN = 4096

[docs] class PathValidationError(ValueError): pass
[docs] def validate_user_path( path: Optional[str], *, kind: str = "any", must_exist: Optional[bool] = None, max_len: int = MAX_PATH_LEN, allow_empty: bool = False, ) -> str: if path is None or path == "": if allow_empty: return "" raise PathValidationError("Path is empty or missing.") if not isinstance(path, str): raise PathValidationError( f"Path must be a string, got {type(path).__name__}." ) if "\x00" in path: raise PathValidationError("Path contains a NULL byte.") if len(path) > max_len: raise PathValidationError( f"Path length {len(path)} exceeds maximum {max_len}." ) canonical = os.path.abspath(path) if must_exist is True: if not os.path.exists(canonical): raise PathValidationError(f"Path does not exist: {path!r}") if kind == "file" and not os.path.isfile(canonical): raise PathValidationError(f"Path is not a regular file: {path!r}") if kind == "dir" and not os.path.isdir(canonical): raise PathValidationError(f"Path is not a directory: {path!r}") elif must_exist is False: if os.path.exists(canonical): raise PathValidationError(f"Path already exists: {path!r}") return canonical
[docs] def ensure_under_base(candidate: str, base: str) -> str: resolved_candidate = os.path.realpath(candidate) resolved_base = os.path.realpath(base) base_prefix = resolved_base.rstrip(os.sep) + os.sep if resolved_candidate != resolved_base and not resolved_candidate.startswith(base_prefix): raise PathValidationError( f"Path escapes base directory: {candidate!r} not inside {base!r}" ) return resolved_candidate
[docs] def sink_safe_path(path: str) -> str: if path is None or path == "": raise PathValidationError("Empty path at sink site.") as_str = os.fspath(path) if "\x00" in as_str: raise PathValidationError("NULL byte in path at sink site.") return os.path.realpath(os.path.abspath(os.path.normpath(as_str)))