"""Attachment storage: validated uploads, safe paths (no traversal), hashing, malware-scan hook.""" import hashlib import os import re import secrets from app.core.config import settings BASE_DIR = settings.UPLOAD_DIR if not os.path.isabs(BASE_DIR): BASE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), BASE_DIR) MIME_BY_EXT = { "pdf": "application/pdf", "html": "text/html", "htm": "text/html", "txt": "text/plain", "png": "image/png", "jpg": "image/jpeg", "jpeg": "image/jpeg", } def allowed_ext(filename: str, allowed: str = None) -> bool: ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "" allowed = (allowed or settings.ALLOWED_ATTACHMENT_EXTS).split(",") return ext in [a.strip().lower() for a in allowed] def safe_join(rel_path: str) -> str: """Resolve a stored relative path under UPLOAD_DIR; refuse traversal.""" root = os.path.realpath(BASE_DIR) candidate = os.path.realpath(os.path.join(root, rel_path)) if not candidate.startswith(root + os.sep) and candidate != root: raise ValueError("Path traversal detected") return candidate def save_upload(filename: str, content: bytes) -> dict: """Store an uploaded attachment with a random name component. Returns metadata.""" os.makedirs(BASE_DIR, exist_ok=True) ext = filename.rsplit(".", 1)[-1].lower() if "." in filename else "bin" safe_name = re.sub(r"[^A-Za-z0-9._-]", "_", filename)[:120] stored = f"{secrets.token_hex(8)}_{safe_name}" path = os.path.join(BASE_DIR, "attachments", stored) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as f: f.write(content) sha = hashlib.sha256(content).hexdigest() return {"stored_path": os.path.join("attachments", stored), "filename": filename, "size": len(content), "sha256": sha, "mime_type": MIME_BY_EXT.get(ext, "application/octet-stream")} def read_attachment_bytes(att) -> bytes | None: try: with open(safe_join(att.stored_path), "rb") as f: return f.read() except Exception: return None def delete_attachment_file(att) -> None: try: os.remove(safe_join(att.stored_path)) except Exception: pass def scan_for_malware(content: bytes, filename: str) -> tuple: """Integration point for AV scanning (e.g. ClamAV). Returns (clean, message). Wire a real scanner here in production (clamd, VirusTotal API, etc.). Heuristic baseline: reject embedded PE/ELF executables regardless of extension. """ if content[:2] == b"MZ" or content[:4] == b"\x7fELF": return False, "Executable file signature detected" return True, "no scanner configured; heuristic pass"