from pathlib import Path, PurePosixPath from urllib.parse import urlsplit SECRET_NAMES = {".env", ".env.local", "id_rsa", "credentials.json", "secrets.yml", "secrets.yaml"} SKIPPED_PARTS = {".git", "node_modules", ".venv", "venv", "build", "dist", "__pycache__"} SECRET_SUFFIXES = {".pem", ".key", ".p12", ".pfx"} def safe_repository_path(root: Path, requested: str) -> Path: if not requested or "\x00" in requested: raise ValueError("Invalid path") candidate = (root / requested).resolve() if root.resolve() not in candidate.parents and candidate != root.resolve(): raise ValueError("Path is outside the repository") if any(part in SKIPPED_PARTS for part in candidate.relative_to(root.resolve()).parts): raise ValueError("Excluded path") if candidate.name.lower() in SECRET_NAMES or candidate.suffix.lower() in SECRET_SUFFIXES: raise ValueError("Sensitive file") return candidate def validate_repository_url(value: str) -> str: parsed = urlsplit(value.strip()) if parsed.scheme != "https" or not parsed.netloc or parsed.username or parsed.password: raise ValueError("Нужна HTTPS-ссылка на Git-репозиторий без токена в URL") if parsed.query or parsed.fragment: raise ValueError("Ссылка на репозиторий не должна содержать параметры или fragment") path = parsed.path.rstrip("/") if path.endswith(".git"): path = path[:-4] if not path or path == "/": raise ValueError("Неполная ссылка на репозиторий") return f"https://{parsed.netloc}{path}.git" def safe_repository_member(requested: str) -> PurePosixPath: path = PurePosixPath(requested) if not requested or path.is_absolute() or ".." in path.parts or "\x00" in requested: raise ValueError("Invalid repository path") if any(part in SKIPPED_PARTS for part in path.parts): raise ValueError("Excluded path") if path.name.lower() in SECRET_NAMES or path.suffix.lower() in SECRET_SUFFIXES: raise ValueError("Sensitive file") return path def allowed_repository_file(path: Path, max_bytes: int) -> bool: try: safe_repository_path(path.parent if path.is_absolute() else Path("."), path.name) return ( path.is_file() and path.stat().st_size <= max_bytes and path.suffix.lower() not in SECRET_SUFFIXES ) except (OSError, ValueError): return False