57 lines
2.5 KiB
Python
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
|