링크를 씻는 손이 여러 곳에서 미끄러졌다
링크를 씻는 손이 여러 곳에서 미끄러졌다
CVE · 웹·서버 · XSS(크로스사이트 스크립팅) · CVE-2026-5388 [표준]
HTML 소독(sanitization) 라이브러리는 “신뢰할 수 없는 마크업에서 위험한 조각만 걸러 안전하게 만든다”는 한 가지 약속으로 존재합니다. justhtml의 1.15.0 이전 버전에서 그 약속이 한 군데가 아니라 여러 지점에서 새어 나갔습니다. 공개된 설명은 URL을 정리하는 도우미 함수(clean_url_value·clean_url_in_js_string), HTML을 문자열로 다시 직렬화하는 과정, Markdown 원문 통과(html_passthrough=True), 그리고 사용자 정의 소독 정책의 몇몇 경계 사례를 함께 지목합니다. 설정에 따라 공격자가 이 틈을 통해 위험한 콘텐츠를 통과시킬 수 있는 것으로 보입니다.
개요
justhtml은 신뢰할 수 없는 HTML을 받아 위험한 요소를 제거·무력화하는 Python(3.10+) HTML5 파서/소독 라이브러리입니다(GitHub EmilStenstrom/justhtml, “sanitize untrusted HTML by default” — 확정).
이번 취약점(CVE-2026-5388)의 특징은 단일 실수가 아니라는 점입니다. 공개 설명은 URL 소독 도우미, HTML 직렬화, Markdown 통과, 사용자 정의 정책의 경계 사례를 여러 개 나열합니다. 즉 “링크를 안전하게 만드는” 경로가 여러 지점에서 미끄러질 수 있고, 소독을 신뢰해 사용자 입력을 페이지에 넣는 애플리케이션이라면 이 틈으로 심어진 스크립트가 피해자 브라우저에서 실행될 수 있습니다.
- 취약 버전: justhtml 1.15.0 미만(0 이상 <1.15.0), 패치 버전 1.15.0 — MITRE·VulnCheck·GHSA-c9vm-hv86-f23r 3중 확정.
- 취약점 유형: HTML/URL 소독 우회 → 크로스사이트 스크립팅. 주 CWE는 CWE-20(부적절한 입력 검증) — CNA(VulnCheck) 권위 할당. GHSA는 부차적으로 CWE-79·CWE-178·CWE-755도 함께 나열합니다.
- 영향 조건: 설정 의존적(configuration-dependent) — VulnCheck 설명: “Most custom-policy issues do not affect the default sanitize=True configuration; they primarily affect helper APIs, programmatic DOM construction, html_passthrough=True, and custom policies.” 즉 기본 설정(
sanitize=True)만 쓰는 배포는 노출이 제한적입니다. - CVSS: v4.0 9.3(CRITICAL)
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N, v3.1 9.8(CRITICAL)CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H— MITRE·VulnCheck 확정.
한 줄 요약: “소독기를 통과했으니 링크도 안전하다”는 가정이, 링크를 씻는 손 여러 개가 동시에 미끄러지면서 무너진 사례입니다.
기술 배경 — URL과 Markdown, 소독기의 두 미끄러운 바닥
HTML 소독에서 태그·속성 필터링만큼 까다로운 것이 URL과 링크 처리입니다. href·src 같은 속성에 들어오는 값은 겉보기엔 평범한 주소지만, javascript: 스킴이나 data: URL, 제어문자·인코딩 트릭을 섞으면 클릭 한 번에 스크립트가 실행되는 통로가 됩니다. 그래서 소독기는 URL을 별도로 “정규화하고 씻는” 도우미를 둡니다 — justhtml의 clean_url_value나, 자바스크립트 문자열 안에 박힌 URL을 다루는 clean_url_in_js_string이 그런 역할을 합니다.
두 번째 미끄러운 바닥은 **Markdown 통과(passthrough)**입니다. 많은 소독기가 Markdown을 HTML로 변환하는데, html_passthrough=True처럼 “원문 HTML을 그대로 통과시키는” 옵션을 켜면 소독 단계를 의도적으로 건너뛴 마크업이 결과에 섞일 수 있습니다. 편의를 위한 옵션이 방어선에 구멍을 내는 전형적인 자리입니다.
세 번째는 **직렬화(serialization)**입니다. 소독기가 입력을 파싱해 안전하다고 판단한 뒤, 그 결과를 다시 문자열 HTML로 뱉는 과정에서 이스케이프가 어긋나면 “파싱할 땐 안전했는데 출력하고 보니 위험한” 재조합이 일어날 수 있습니다.
CVE 분석 요약 — 어디서 새는가
공개 설명은 우회 지점을 한 곳이 아니라 여러 곳으로 나열합니다. 벤더 GHSA-c9vm-hv86-f23r가 실제로 확인한 우회 유형은 다음과 같습니다(공개 자문이 확인한 사실 — 파일·라인 단위 소스 대조는 심층판 몫이라 표준판 범위 밖입니다):
- 인코딩된
javascript:URL: 인코딩·공백 트릭이 섞인javascript:스킴이 URL 소독 도우미(clean_url_value·clean_url_in_js_string)를 통과. - 백슬래시 기반 상대 URL: 백슬래시를 쓴 상대 경로가 브라우저에서는 원격 호스트로 해석됨.
- 프로그래매틱 DOM 구성의 요소/속성 이름·HTML 주석: 프로그램적으로 DOM을 만들 때 요소·속성 이름이나 HTML 주석을 이용해 마크업 구조를 깨뜨림.
- Markdown 원문 통과(
html_passthrough=True)를 통한 raw</textarea>재도입: 편의 옵션이 소독되지 않은 마크업을 그대로 결과에 섞음. - 커스텀 정책의
<style>/<meta http-equiv=refresh>/<base href>보존: 기본 정책이 아니라 사용자가 넓힌 커스텀 소독 정책의 특정 조합에서 방어가 뚫림.
아래는 “링크를 씻었다고 믿었는데 실행 경로가 남는다”는 개념을 보여주는 무해한 예시입니다. 실제 우회 문자열이 아니며, 실행되지 않도록 무력화했습니다.
# 개념용 단순화, 실제 페이로드/원본 코드 아님 (무해화)
입력(신뢰 불가): <a href=" 위험스킴 : ...무해화... ">클릭</a>
소독기의 판단: "정규화했고 스킴 목록에 없음 → 안전한 링크" → 통과시킴
브라우저의 해석: "앞쪽 공백/인코딩을 무시하고 스킴을 인식" → 실행 경로 성립
└── '씻는 규칙'과 '읽는 규칙'이 어긋나는 지점이 곧 우회 지점
즉 원인은 “무엇을 차단 목록에 넣었나”보다 “URL을 씻는 규칙이 브라우저가 URL을 읽는 규칙과 일치하는가”, 그리고 **“편의 옵션(통과·커스텀 정책)이 방어선을 우회하도록 열어두지 않았나”**에 가깝습니다. 목록·정규화 기반 방어는 파서·정규화 불일치 앞에서 늘 취약합니다.
공격 시나리오 · 영향도 + 대응방안
공격 시나리오 (단계 압축, 완성형 페이로드 없음):
- 공격자가 소독을 거치는 입력란(댓글·프로필·Markdown 메모 등)에 우회를 유도하는 변형 링크/마크업을 넣습니다.
- 애플리케이션이 justhtml로 “소독·정리했으니 안전하다”고 판단하고 그 결과를 페이지에 그대로 렌더링합니다.
- 피해자가 그 페이지를 열거나 링크를 상호작용하면, 소독을 통과해 살아남은 실행 경로가 피해자 브라우저에서 동작합니다.
영향도: 저장형·반사형 XSS로 이어질 경우, 세션 탈취·계정 조작·피해자 권한 내 임의 동작·피싱 발판 등 XSS의 일반적 영향이 성립할 수 있습니다. 공개 설명이 확정하는 바로는, 기본 설정(sanitize=True)만 쓰는 대다수 배포는 이 이슈들의 영향을 받지 않으며, html_passthrough 같은 통과 옵션이나 넓힌 커스텀 정책을 쓰는 배포에서 노출이 집중됩니다(VulnCheck 확정).
대응방안:
- 근본 — 패치 버전으로 업데이트. justhtml 1.15.0 이상으로 올립니다(MITRE·VulnCheck·GHSA 3중 확정 패치 버전). 소독기 우회는 “일부만 막고 변종이 남는” 경우가 잦으므로, 이후 벤더 릴리스 노트에서 후속 재패치 공지가 없는지 계속 확인하는 것을 권합니다.
- 차선(즉시 업데이트 불가 시):
- 원문 HTML 통과 옵션을 끕니다.
html_passthrough=True처럼 소독을 건너뛰는 경로를 사용 중이면 비활성화하거나 통과 대상을 최소화합니다. - 소독 정책을 좁힙니다. 넓힌 커스텀 태그·속성·스킴 허용을 기본 프로파일에 가깝게 되돌리고,
href/src에 대해 허용 스킴을 화이트리스트(http/https/mailto등)로 제한합니다. - 소독 단독에 의존하지 않습니다. 출력 지점에서 문맥에 맞는 인코딩을 병행하고, 콘텐츠 보안 정책(CSP, Content-Security-Policy)으로 인라인 스크립트 실행을 제한해 우회가 실제 실행으로 번지는 걸 막습니다.
- 원문 HTML 통과 옵션을 끕니다.
- 탐지 관점: 소독을 거친 뒤에도 출력의
href/src에 위험 스킴(javascript:·의심스러운data:)이나 이벤트 핸들러가 남는지 표본 검사·회귀 테스트로 상시 점검합니다. CSP 위반 리포트를 수집하면 우회 시도를 사후에 포착하는 데 도움이 됩니다.
결론
소독기는 방패지만, 방패의 이음매는 “링크를 씻는 규칙과 브라우저가 링크를 읽는 규칙이 같은가”, 그리고 “편의를 위해 열어둔 통과 옵션이 방어선을 관통하지 않는가”에 달려 있습니다. justhtml 1.15.0 이전의 이 우회들은 그 이음매가 여러 곳에서 벌어진 자리였습니다.
이 부류의 버그가 반복되는 이유는 분명합니다. 소독은 “가장 위험한 값(URL·통과 옵션·커스텀 정책)을 예외로 두는 순간” 무너지기 때문입니다. 목록과 정규화를 아무리 다듬어도, 씻는 규칙과 읽는 규칙이 어긋나거나 편의 옵션이 방어를 건너뛰는 한 틈은 다른 자리에서 또 열립니다. 그래서 우리는 “무엇을 필터에 넣었나”가 아니라 “소독을 단일 방어선으로 두지 않았나, 통과 옵션이 구멍을 내지 않았나”를 먼저 봅니다 — 이 한 틈은 패치로 닫히겠지만, 이 사고방식은 다음 소독기에서 또 필요합니다. 그래서 도구가 아니라 방법을 남깁니다.
(이 CVE의 심층 분석판은 아직 없습니다. 소스 대조·패치 diff까지 파는 심층판으로 승급하면 여기에 링크됩니다. 같은 라이브러리의 다른 소독 우회 사례는 CVE-2026-7808 표준판과 역링크됩니다.)
References / 참고자료
- MITRE CVEAWG — CVE-2026-5388 공식 레코드. 영향 제품·버전·CVSS·CWE 근거. https://cveawg.mitre.org/api/cve/CVE-2026-5388
- VulnCheck advisory — CNA(할당 기관) 원문. 취약/패치 버전(<1.15.0 / 1.15.0)·CVSS 이중값(v4.0 9.3, v3.1 9.8)·CWE-20 주분류·설정 의존적 영향범위 확정 근거. https://www.vulncheck.com/advisories/justhtml-before-multiple-security-issues
- GHSA-c9vm-hv86-f23r (“Multiple security fixes in justhtml 1.14.0 found by LLM”) — 벤더(justhtml 저장소) 자문. 우회 유형(인코딩 URL·백슬래시 상대URL·DOM 구성 우회·Markdown passthrough·커스텀 정책 보존) 확증 근거.
- OWASP, “Cross Site Scripting (XSS)” 및 “DOM based XSS Prevention / HTML Sanitization” 가이드 — URL 스킴 검증·파서 불일치·출력 인코딩·CSP의 일반 원리 배경.
이 글은 위 1차출처(MITRE CVEAWG·VulnCheck advisory·GHSA-c9vm-hv86-f23r)를 우리 관점에서 다시 쓰고 논평을 더한 것입니다. 특정 문장·코드를 그대로 옮기지 않았으며, 자세한 재현 절차는 원문을 참고하세요. 이 초안은 RN-11 독립 팩트체크(2026-08-31)를 통과했습니다.