✍️ 기록/React

[React] 문자열로 HTML 구조 데이터 다룰 때 DOMPurify 안전하게 사용하기

김물사 2025. 4. 9. 14:11

📌 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>
  )
}

 

✅ 정상적으로 렌더링 완료!

 

 

 

✍️ 기록

 

감사합니다. 😁

반응형