FE
[SvelteKit] 신뢰할 수 있는 마크다운 뷰어 만들기
| DOMPurify로 XSS 취약점 완벽히 해결하기
안녕하세요 팡일입니다.
현재 제작중인 회고 서비스, 'Relog'에서 발견한 취약점 중 하나를 개선한 이야기를 공유해보려고 합니다. 이전에는 임시 주석을 달아서 해결하려고 했지만, 이 부분으로 해결할 수 없는 문제점들이 있었습니다.
이를 조금 더 설명해보면, 웹 서비스에서 “사용자가 입력한 내용을 HTML로 렌더링한다”는 것은 생각보다 위험한 일입니다. 특히 마크다운(markdown)을 HTML로 변환해 보여주는 기능은 겉보기엔 단순하지만, 잘못 구성하면 곧바로 XSS 취약점으로 이어질 수 있습니다.
바로 이 부분에서 보안상의 심각한 문제가 숨어 있다는 것을 발견했습니다. 사용자 회고 내용을 마크다운으로 렌더링하는 과정이 아무런 보호 없이 그대로 노출되어 있었던 것이죠.
이번 글에서는 취약점을 발견한 과정부터 DOMPurify를 이용해 완벽하게 해결한 방법까지 차근차근 공유해보려고 합니다.
| 문제의 시작: 마크다운 렌더러에 숨어 있던 XSS
프로젝트에서 사용 중이던 markdown-it 설정은 다음과 같았습니다.
const md = new MarkdownIt({
html: true,
linkify: true
});여기서 문제는 바로 html: true 옵션입니다. 이 옵션은 “사용자가 입력한 HTML 태그를 그대로 렌더링해도 된다”는 의미입니다.
즉, 사용자가 다음과 같이 악의적인 HTML을 넣어도 그대로 실행된다는 뜻입니다.
오늘의 회고입니다.
<img src="x" onerror="alert('해킹 성공!')">그리고 저는 해당 HTML을 아래와 같은 식으로 렌더링하고 있었습니다:
{@html renderMarkdown(content)}이 조합이 만들어내는 결과는 매우 위험합니다. 특히 공유 페이지처럼 로그인 없이도 접근 가능한 페이지에 XSS가 노출되어 있었다는 점에서, 보안 사고로 직결될 수 있는 수준의 문제였습니다.
| 해결책 선택: DOMPurify로 HTML 완전 소독
XSS를 방어하는 대표적인 방식은 다음 두 가지입니다.
아예 HTML을 제거해버린다.
HTML 중 ‘안전한 부분만’ 허용하고 나머지는 제거한다.
저는 사용자 경험을 유지하기 위해 2번을 선택했고, 업계 표준인 DOMPurify를 적용했습니다.
설치는 간단합니다.
npm install dompurify
npm install -D @types/dompurify@types/dompurify는 타입스크립트 지원을 위해 함께 설치했습니다.
| 마크다운 렌더링 파이프라인에 “소독 과정” 추가하기
가장 중요한 변경은 renderMarkdown 함수에 DOMPurify를 통과시키는 부분을 추가하는 것이었습니다.
1) 수정 전
export function renderMarkdown(markdown: string): string {
return md.render(markdown);
}2) 수정 후
import { browser } from '$app/environment';
import MarkdownIt from 'markdown-it';
import DOMPurify from 'dompurify';
export function renderMarkdown(markdown: string): string {
const renderedHtml = md.render(markdown);
if (browser) {
return DOMPurify.sanitize(renderedHtml);
}
return renderedHtml;
}여기서 if (browser) 체크가 매우 중요합니다.
DOMPurify는 DOM API를 사용합니다.
서버 렌더링(SSR) 환경에서는 DOM이 존재하지 않습니다.
때문에 브라우저에서만 sanitize를 수행하고, SSR에서는 순수 HTML만 생성하도록 처리했습니다.
이 구조 덕분에 SvelteKit의 SSR + CSR 환경에서도 안정적으로 동작하게 됩니다.
| 적용 후 얻은 효과
이 변경으로 얻은 이점은 강력했습니다.
1) XSS 완전 차단
: <script> 태그, 이벤트 핸들러, 악성 속성 등은 모두 제거되기 때문에 어떠한 HTML 기반 공격도 실행될 수 없습니다.
2) 사용자 신뢰도 상승
: 사용자가 자신의 회고를 안심하고 기록할 수 있는, 기본기가 탄탄한 플랫폼이 되었습니다.
3) 기능과 보안의 균형
:html: true는 유지했기 때문에 <b>, <i>, <u> 등 무해한 서식 태그는 계속 사용할 수 있습니다. DOMPurify가 위험 요소만 걸러주기 때문입니다.
| 결론: “사용자의 입력은 절대 신뢰하지 않는다”
이번 경험을 통해 가장 크게 느낀 점은 단 하나입니다.
-> 사용자가 입력하는 모든 데이터는 반드시 검증·소독되어야 한다.
특히 { @html } 같은 기능을 사용하는 순간, 우리는 렌더링되는 모든 문자열에 대한 책임을 져야 합니다.
SvelteKit에서 마크다운을 렌더링하고 있다면, 그리고 그 과정에서 HTML을 허용하고 있다면, 지금 바로 DOMPurify나 유사한 Sanitizer를 도입해보시길 적극 추천드립니다. 여러분의 서비스도 저처럼 어느 순간 치명적인 보안 구멍을 발견하기 전에, 미리 예방할 수 있기를 바랍니다.