# backend/app/security/sanitize.py — 서버측 HTML 새니타이즈 (저장 시 1차 방어, 의존성 없음) # 시드 HTML(briefingNote/notes/coach)은 신뢰. 사용자 입력·LLM 생성·외부 콘텐츠 경로만 통과. import re _ALLOWED_TAGS = { "b", "strong", "i", "em", "u", "br", "p", "h3", "blockquote", "ul", "ol", "li", "a", "code", } _TAG_RX = re.compile(r"<\s*(/?)\s*([a-zA-Z0-9]+)([^>]*)>") _HREF_RX = re.compile(r'href\s*=\s*"([^"]*)"', re.IGNORECASE) _SCRIPT_RX = re.compile(r"<\s*(script|style)[^>]*>.*?<\s*/\s*\1\s*>", re.IGNORECASE | re.DOTALL) def _clean_attrs(tag: str, attrs: str) -> str: if tag != "a": return "" m = _HREF_RX.search(attrs or "") if not m: return "" href = m.group(1) if href.lower().startswith(("javascript:", "data:", "vbscript:")): return "" return f' href="{href}" rel="noopener noreferrer"' def sanitize_html(raw: str) -> str: if not raw: return "" raw = _SCRIPT_RX.sub("", raw) def repl(m: re.Match) -> str: closing, tag, attrs = m.group(1), m.group(2).lower(), m.group(3) if tag not in _ALLOWED_TAGS: return "" # 비허용 태그 제거(내용은 보존) if closing: return f"" return f"<{tag}{_clean_attrs(tag, attrs)}>" return _TAG_RX.sub(repl, raw)