You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
52 lines
1.4 KiB
Python
52 lines
1.4 KiB
Python
# 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)
|