소독기를 믿었는데, 위험한 태그가 그대로 살아 나왔다
소독기를 믿었는데, 위험한 태그가 그대로 살아 나왔다
CVE · 웹·서버 · XSS(크로스사이트 스크립팅) · CVE-2026-7808 [표준]
HTML 소독(sanitization) 라이브러리는 “사용자가 준 마크업에서 위험한 조각만 걸러내 안전하게 만든다”는 한 가지 약속으로 존재합니다. justhtml의 1.16.0 이전 버전에서 그 약속이 여러 지점에서 새어 나갔습니다. <script>나 <style> 같은 능동/위험 콘텐츠가 소독을 통과해 살아남을 수 있었고, 그 결과 브라우저에서 스크립트가 실행되는 크로스사이트 스크립팅(XSS)으로 이어질 수 있었습니다. 공개된 설명에 따르면 이 문제는 기본 설정보다는 고급 사용(advanced usage) 시나리오에 주로 영향을 준다고 합니다.
개요
justhtml은 신뢰할 수 없는 HTML을 받아 위험한 요소를 제거하는 Python 라이브러리입니다(PyPI pip justhtml, 제작자 EmilStenstrom — 확정). 이번 취약점(CVE-2026-7808)의 핵심은 소독기가 걸러야 할 능동 콘텐츠(스크립트·스타일 등)를 특정 조건에서 걸러내지 못하고 통과시킨다는 점입니다. 소독을 신뢰해 사용자 입력을 그대로 페이지에 넣는 애플리케이션이라면, 이 우회를 통해 공격자가 심은 스크립트가 피해자 브라우저에서 실행될 수 있습니다.
- 취약 버전: justhtml 1.16.0 미만, 패치 버전 1.16.0 — NVD·OSV 확정.
- 취약점 유형: HTML 소독 우회 → 크로스사이트 스크립팅. **NVD 주 분류는 CWE-20(부적절한 입력 검증)**이며, 유지관리자 자문은 CWE-20·CWE-79·CWE-178·CWE-436·CWE-471을 함께 명시합니다(CWE-79도 실제 할당 목록에 포함 — 확정).
- 영향 조건: 기본 설정보다 고급 사용(advanced usage) 시나리오 위주 — NVD 원문 “primarily affect advanced usage rather than the default JustHTML(…, sanitize=True) path”로 확정.
- CVSS: 여기서 출처 간 실질적 불일치가 확인됩니다. NVD는 v3.1 9.8(CRITICAL)
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H, **v4.0 9.3(CRITICAL)**로 채점한 반면, 유지관리자 자신의 자문(GHSA-4p64-v8f5-r2gx)은 LOW로 채점합니다(CVSS:4.0/.../UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:U). NVD는 일반적인 사전인증 XSS급으로 채점했고, 유지관리자는 “고급 경로 전용”으로 스코프를 좁혀 채점한 차이입니다 — 두 값 모두 병기합니다.
한 줄 요약: “소독기를 통과했으니 안전하다”는 가정이, 소독기 자신의 사각지대 때문에 무너진 사례입니다. 다만 그 심각도를 얼마나 볼지는 NVD와 유지관리자의 평가가 갈립니다.
기술 배경 — 소독은 “허용”과 “거부” 사이의 줄다리기
HTML 소독기의 일은 개념적으로 두 가지입니다. (1) 허용할 태그·속성만 남기고 나머지를 제거하거나 무력화하고, (2) 남긴 것 안에 숨은 실행 경로(예: 인라인 이벤트 핸들러, javascript: URL, 스타일 안의 표현식)를 차단하는 것입니다.
문제는 브라우저의 HTML 파싱이 관대하고 예외가 많다는 데 있습니다. 소독기가 “이건 안전한 텍스트”라고 판단한 조각을, 실제 브라우저는 “이건 실행 가능한 마크업”으로 다르게 해석할 수 있습니다. 소독기의 파싱 모델과 브라우저의 파싱 모델이 어긋나는 지점이 곧 우회 지점입니다. <script>나 <style> 같은 원시 텍스트(raw text) 요소, 주석·CDATA 경계, 중첩·오파싱을 유도하는 변형된 태그가 특히 단골 사각지대입니다.
<style>이 위험 목록에 함께 오르는 이유도 여기 있습니다. 스타일 블록은 단순 꾸밈처럼 보이지만, 로딩·표현식·선택자 트릭을 통해 정보 유출이나 스크립트 실행의 발판이 될 수 있어 능동 콘텐츠로 취급됩니다.
CVE 분석 요약 — 어디서 새는가
공개된 설명은 “여러 개의 소독 우회(multiple bypass issues)“라고 밝히고 있어, 단일 실수가 아니라 소독 로직의 여러 사각지대가 함께 지적된 것입니다. 유지관리자 자문(GHSA-4p64-v8f5-r2gx)이 확인한 실제 우회 경로는 다음 다섯 가지입니다(추정이 아니라 1차출처 확정 사실):
- 소독 정책 객체의 변조·재사용:
sanitize()/sanitize_dom()에 넘기는 정책(policy) 객체를 변조하거나 재사용해 방어 규칙이 의도와 다르게 적용됨. - 대소문자를 섞은 태그 이름(mixed-case): 프로그래매틱 DOM 입력에
ScRiPt·StYlE처럼 대소문자를 섞은 태그 이름을 넣어 소독 로직의 태그 매칭을 우회. - 조작된 doctype 이름: doctype 이름을 조작해 직렬화 시 능동 마크업으로 다시 살아남.
- 커스텀 정책의 SVG/MathML 외래 콘텐츠 보존: 커스텀 정책이 SVG/MathML의 애니메이션 요소·
url(...)표현 속성 같은 외래(foreign) 콘텐츠를 그대로 보존. - raw-text 강화의 사각지대:
<script>/<style>같은 raw-text 요소에서 대소문자를 섞은 경우 강화된 방어가 놓침(초안이 “원시 텍스트 요소의 오처리”로 추정했던 부분과 부분적으로 부합).
아래는 “소독기와 브라우저가 같은 입력을 다르게 읽는다”는 개념을 보여주는 무해한 예시입니다. 실제 우회 문자열이 아니며, 실행되지 않도록 무력화했습니다.
# 개념용 단순화, 실제 페이로드/원본 코드 아님 (무해화)
입력(신뢰 불가): <위험태그> ...내용... </다르게-닫힘>
소독기의 해석: "닫는 태그가 안 맞네 → 전부 무해한 텍스트" → 통과시킴
브라우저의 해석: "이건 능동 요소다 → 내용 실행" → XSS 성립
└── 두 해석이 '어긋나는' 지점이 곧 우회 지점
즉 원인은 “필터 목록에 무엇을 넣었나”보다 **“소독기의 파싱이 브라우저의 파싱과 일치하는가”**에 가깝습니다. 목록 기반 차단은 파서 불일치(parser differential) 앞에서 늘 취약합니다.
공격 시나리오 · 영향도 + 대응방안
공격 시나리오 (단계 압축, 완성형 페이로드 없음):
- 공격자가 소독을 거치는 입력란(댓글·프로필·메모 등)에 우회를 유도하는 변형 마크업을 넣습니다.
- 애플리케이션이 justhtml로 “소독했으니 안전하다”고 판단하고 그 결과를 페이지에 그대로 렌더링합니다.
- 피해자가 그 페이지를 열면, 소독을 통과해 살아남은 능동 콘텐츠가 피해자 브라우저에서 실행됩니다.
영향도: 저장형·반사형 XSS로 이어질 경우, 세션 탈취·계정 조작·피해자 권한 내 임의 동작·피싱 발판 등 XSS의 일반적 영향이 성립할 수 있습니다(구체 영향은 애플리케이션이 소독 결과를 어디에 어떻게 쓰는지에 달려 있습니다). 다만 실제 채점은 갈립니다 — NVD는 일반적 사전인증 XSS급(9.8/9.3)으로 평가한 반면, 유지관리자는 이 이슈들이 고급 사용 경로에 한정된다는 근거로 자체 심각도를 LOW로 채점했습니다. 기본 프로파일(sanitize=True)만 쓰는 배포는 유지관리자 평가에 따르면 영향이 제한적입니다.
대응방안:
- 근본 — 패치 버전으로 업데이트. justhtml 1.16.0 이상으로 올립니다(NVD·OSV 확정 패치 버전). 소독기 우회는 “일부만 막고 변종이 남는” 경우가 잦으므로, 이후 벤더 릴리스 노트에서 후속 재패치 공지가 없는지 계속 확인하는 것을 권합니다.
- 차선(즉시 업데이트 불가 시):
- 허용 목록을 가능한 한 좁게 유지합니다. 특히 문제와 관련된 고급/확장 설정(넓힌 태그·속성 허용)을 잠시 기본 프로파일로 되돌립니다.
- 소독 단독 방어에 의존하지 않습니다. 출력 지점에서 문맥에 맞는 인코딩을 병행하고, 콘텐츠 보안 정책(CSP, Content-Security-Policy)으로 인라인 스크립트 실행을 제한해 우회가 실제 실행으로 번지는 걸 막습니다.
- 사용자 HTML을 신뢰 도메인과 분리해 렌더링(예: 격리된 iframe·별도 오리진)하는 방안을 검토합니다.
- 탐지 관점: 소독을 거친 뒤에도
<script>/<style>/이벤트 핸들러/javascript:스킴 등 능동 콘텐츠 흔적이 출력에 남는지 표본 검사·회귀 테스트로 상시 점검합니다. CSP 위반 리포트를 수집하면 우회 시도를 사후에 포착하는 데 도움이 됩니다.
결론
소독기는 방패지만, 방패의 이음매는 “소독기가 세상을 브라우저와 똑같이 읽는가”에 달려 있습니다. justhtml 1.16.0 이전의 이 우회들은 그 이음매가 벌어진 자리였습니다.
이 부류의 버그가 반복되는 이유는 분명합니다. 목록 기반 소독은 파서 불일치를 이길 수 없기 때문입니다. 허용/거부 목록을 아무리 다듬어도, 소독기와 브라우저가 같은 바이트를 다르게 파싱하는 한 틈은 다른 자리에서 또 열립니다. 그래서 우리는 “무엇을 필터에 넣었나”가 아니라 “소독을 단일 방어선으로 두지 않았나”를 먼저 봅니다 — 이 한 틈은 패치로 닫히겠지만, 이 사고방식은 다음 소독기에서 또 필요합니다. 그래서 도구가 아니라 방법을 남깁니다.
(이 CVE의 심층 분석판은 아직 없습니다. 소스 대조·패치 diff까지 파는 심층판으로 승급하면 여기에 링크됩니다.)
References / 참고자료
- NVD JSON API — CVE-2026-7808 공식 레코드(등재 2026-08-23). 취약/패치 버전(<1.16.0 / 1.16.0)·CVSS(v3.1 9.8, v4.0 9.3)·CWE-20 주분류 근거.
services.nvd.nist.gov/rest/json/cves/2.0?cveId=CVE-2026-7808 - GHSA-4p64-v8f5-r2gx — justhtml 저장소 유지관리자(EmilStenstrom) 자신의 GitHub Security Advisory. 우회 5유형(정책 변조·mixed-case 태그·doctype 조작·SVG/MathML 보존·raw-text 강화 사각지대) 확증, 자체 심각도 LOW 채점 근거.
- OSV — GHSA-4p64-v8f5-r2gx 미러(취약/패치 버전 교차확인).
- OWASP, “Cross Site Scripting (XSS)” 및 “HTML Sanitization” 가이드 — 소독 우회·파서 불일치·출력 인코딩·CSP의 일반 원리 배경.
이 글은 위 1차출처(NVD JSON API·유지관리자 GHSA-4p64-v8f5-r2gx·OSV)를 우리 관점에서 다시 쓰고 논평을 더한 것입니다. 특정 문장·코드를 그대로 옮기지 않았으며, 자세한 재현 절차는 원문을 참고하세요. 이 초안은 RN-11 독립 팩트체크(2026-08-31)를 통과했습니다.