# 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"{tag}>"
return f"<{tag}{_clean_attrs(tag, attrs)}>"
return _TAG_RX.sub(repl, raw)