[React] 문자열로 HTML 구조 데이터 다룰 때 DOMPurify 안전하게 사용하기
📌 HTML 구조 문자열 사용하기
export const TestPage = () => {
const testData = '<strong>Tistory</strong> <br />테스트';
return(
<div className="test-wrap">
<p><strong>STRONG</strong><br /> 테스트 </p>
<p>{testData}</p>
</div>
)
}
✅ testData에 입력된 html 태그는 그냥 문자열로 렌더링이 되어 태그를 그대로 확인할 수 있어요.
이유는 React는 기본적으로 모든 출력값을 이스케이프(escape)하여 처리하는데 XSS(크로스사이트스크립팅) 공격을 방지하기 위한 기본 보안 기능이라고 합니다!
{ } 를 사용하여 변수나 표현식을 렌더링할 때 항상 일반 텍스트로 취급하여 HTML, 스크립트로 해석하지 않습니다.
📍 React HTML로 렌더링 하기 dangerouslySetInnerHTML
⚠️ dangerouslySetInnerHTML 속성을 사용하면 HTML 구조를 렌더링 가능
- dangerously 이름에서 알 수 있듯 경고하고 있습니다.
export const TestPage = () => {
const testData = '<strong>Tistory</strong> <br />테스트';
return(
<div className="test-wrap">
<p dangerouslySetInnerHTML={{ __html: testData }} />
</div>
)
}
✅ dangerouslySetInnerHTML 속성을 사용하면 HTML 구조로 렌더링 성공
📍 XSS란?
- XSS(Cross Site Scripting)는 악의적인 사용자가 자바스크립트 코드나 해로운 HTML을 주입해
사이트 방문자의 정보를 탈취하거나, 원하지 않는 행동을 유도하는 공격 방식
export const TestPage = () => {
const testData = `
<p>위험한 스크립트 포함</p>
<a href="javascript:alert('공격!! XSS!')">클릭</a>
<img src="terror" onerror="alert('공격!! XSS!')" />
`;
return(
<div className="test-wrap">
<p dangerouslySetInnerHTML={{ __html: testData }} />
</div>
)
}
⚠️ 악의적인 스크립트를 삽입하게 되면 쿠키, 세션 등의 탈취로 인한 보안 위협이 발생
✅ 위와 같은 문제를 해결하기 위해 사용하는 대표적인 라이브러리 DOMPurify
📍 DOMPurify 설치
npm install dompurify
// TypeScript
npm install --save-dev @types/dompurify
📍 DOMPurify 사용 방법
export const TestPage = () => {
const testData = `
<p>
안전하게 사용할 수 있는 <br />
<strong>DOMPurify.sanitize</strong> HTML 구조 <br />
<a href="javascript:alert('공격!! XSS!')">클릭</a> <br />
<img src="terror" onerror="alert('공격!! XSS!')" />
</p>
`;
const sanitizeData = DOMPurify.sanitize(testData);
return(
<div className="test-wrap">
<p dangerouslySetInnerHTML={{ __html: sanitizeData }} />
</div>
)
}
// 🔽 utils
import DOMPurifyfrom 'dompurify';
export function sanitizeHtml(dataHTML: string) {
return DOMPurify.sanitize(dataHTML);
}
// 🔽 components
import React from 'react';
import DOMPurify from 'dompurify';
export const SanitizeHtml = ({ dataHTML }) => {
const sanitizeData = DOMPurify.sanitize(dataHTML);
return <div dangerouslySetInnerHTML={{ __html: sanitizeData }} />;
}
✅ a 태그의 경우 href="일반 경로 통과" javascript 관련 삭제! onerror 삭제 되어 안전하게 렌더링 성공
- DOMPurify 사용 시 보안을 지키기 위해 항상 최신 버전을 유지해야해요!
📍 DOMPurify 설정 옵션
ALLOWED_TAGS | 허용할 HTML 태그 목록 |
ALLOWED_ATTR | 허용할 HTML 속성 목록 |
ADD_TAGS | 기본적으로 허용되지 않는 태그를 추가 |
ADD_ATTR | 기본적으로 허용되지 않는 속성을 추가 |
FORBID_TAGS | 특정 태그 금지 |
FORBID_ATTR | 특정 속성 금지 |
KEEP_CONTENT | 태그는 제거하지만 내용은 유지할지 여부 |
WHOLE_DOCUMENT | 전체 HTML 문서를 정화할지 여부 |
📍 iframe 테스트
// DOMPurify ❌
export const TestPage = () => {
const iframeTest = '<iframe src="http://localhost:4000" width="350" height="350" frameborder="0"></iframe>';
return(
<div className="test-wrap">
TEST
<p dangerouslySetInnerHTML={{ __html: iframeTest }} />
</div>
)
}
✅ iframe 정상적으로 나오지만 안전하지 않은 상태
// ✅ DOMPurify 사용
export const TestPage = () => {
const iframeTest = '<iframe src="http://localhost:4000" width="350" height="350" frameborder="0"></iframe>';
const useSanitizeHtml = (dataHTML: string) => {
return useMemo(() => DOMPurify.sanitize(dataHTML), [dataHTML]);
}
return(
<div className="test-wrap">
TEST
<p dangerouslySetInnerHTML={{ __html: useSanitizeHtml(iframeTest) }} />
</div>
)
}
✅ DOMPurify는 iframe 태그 자체 허용하지 않아 제거가 되는데 사용하기 위해서는 허용하는 태그에 iframe 추가와 iframe에서 사용하는 속성도 추가를 해야 정상적으로 동작이 가능
import DOMPurify, { Config } from 'dompurify'; // 👈 Config
import { useMemo } from 'react';
export const TestPage = () => {
const iframeTest = '<iframe src="http://localhost:4000" width="350" height="350" frameborder="0"></iframe>';
// EX)
const useSanitizeHtml = ( dataHTML: string, options?: Config ) => {
return useMemo(() => {
return DOMPurify.sanitize(dataHTML, options);
}, [dataHTML, options]);
};
return(
<div className="test-wrap">
TEST
<p dangerouslySetInnerHTML={{ __html: useSanitizeHtml(iframeTest, {
ADD_TAGS: ['iframe'], // 허용 태그
ADD_ATTR: ['allow', 'allowfullscreen', 'frameborder', 'scrolling', 'src'], // 허용 옵션
})}} />
</div>
)
}
✅ 정상적으로 렌더링 완료!
✍️ 기록
감사합니다. 😁