Files
relay-bot/app/repository/security.py
T
2026-07-24 22:36:04 +03:00

57 lines
2.5 KiB
Python

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