리액트에서 dangerouslySetInnerHTML 사용하기

FeRo 페로·2023년 7월 17일
1

dangerouslySetInnerHTML과 관련된 내용을 찾아보다가 잘 정리된 글이 있어서 번역하며 공부해 보았다. 거의 대부분의 번역하였지만 몇몇 부분은 중복된 내용이거나 크게 중요하지 않은 내용이라 생략했다. 원문은 아래 링크를 참고하면 된다.

Using dangerouslySetInnerHTML in a React application - LogRocket Blog

리액트에서 dangerouslySetInnerHTML 사용하기

selector를 사용해서 HTML element를 가져온 다음, innerHTML을 사용해서 설정하는 대신에 dangerouslySetInnerHTML을 사용하면 element를 직접 사용할 수 있습니다. dangerouslySetInnerHTML를 사용하면 리액트는 특정 엘리먼트가 동적이라는 것을 알고, 그 노드의 children에 대한 virtualDOM과의 비교를 건너뛰게 됩니다. 그로인한 추가적인 성능을 얻을 수 있습니다.

하지만 이름에서 알 수 있듯이, 이걸 사용하면 XSS 공격에 취약해 질 수 있습니다. 유저가 제출한 컨탠츠를 렌더링 하거나 서드파티 소스로부터 데이터를 fetching한다면 이 부분이 문제가 됩니다.

const App = () => {
  const data = 'lorem <b>ipsum</b>';

  return (
    <div>
      {data}
    </div>
  );
}

export default App;
const App = () => {
  const data = 'lorem <b>ipsum</b>';

  return (
    <div
      dangerouslySetInnerHTML={{__html: data}}
    />
  );
}

export default App;

_html키를 가진 object형태로 dangerouslySetInnerHTML에 넘겨야 합니다. 다른 부분으로는 dangerouslySetInnerHTML을 사용하는 element는 어떠한 children도 없어야 하므로 <div> elementself-closing tag로서 사용해야 합니다.

객체로 넘기는 것은 개발자가 documentation을 살펴보지 않아서 발생하는 잠재적인 위험을 방지하기 위한 안전장치 입니다.

안전하게 dangerouslySetInnerHTML 사용하기

위의 간단한 예시에서는 어떤 위험도 없습니다. 그러나 HTML element가 스크립트를 실행하는 경우가 있습니다. 비록 예시가 멀쩡해 보여도 어떻게 html element가 악성 스크립트를 작동시킬 수 있는지 증명할 수 있습니다. 아래 예시에서는 ipsum 부분에 마우스가 호버링 되면 alert가 작동됩니다.

const App = () => {
  const data = `lorem <b>ipsum</b>`;

  return (
    <div
      dangerouslySetInnerHTML={{__html: data}}
    />
  );
}

export default App;

const App = () => {
  const data = `lorem ipsum <img src="" />`;

  return (
    <div
      dangerouslySetInnerHTML={{__html: data}}
    />
  );
}

export default App;

다행히도 html 코드에서 잠재적인 악성코드를 추적해서 안전한(sanitized: 소독된) 버전으로 출력하게 해주는 도구가 있습니다. DOMPurify 입니다.

// Original
lorem <b onmouseover="alert('mouseover');">ipsum</b>

// Sanitized
lorem <b>ipsum</b>
// Original
lorem ipsum <img src="" onerror="alert('message');" />

// Sanitized
lorem ipsum <img src="">

바로 DOMPurify를 사용하는 것입니다.

import DOMPurify from 'dompurify'

const App = () => {
  const data = `lorem <b>ipsum</b>`
  const sanitizedData = () => ({
    __html: DOMPurify.sanitize(data)
  })

  return (
    <div
      dangerouslySetInnerHTML={sanitizedData()}
    />
  );
}

export default App;

예상대로, sanitize가 되면서 bold text에 호버링을 해도 작동되는 함수가 없습니다.

DOMPurify는 DOM tree가 필요한데, 노드 환경에서는 돔 트리가 없기 때문에 우리는 jsdom 패키지를 사용해서 window 객체를 만들고 DOMPurify를 초기화 해야 합니다. 또는 DOMPurify나 jsdom 패키지를 캡슐화한 isomorphic-dompurify 패키지를 사용해야 합니다.

const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

const clean = DOMPurify.sanitize(dirty);

마무리

innerHTML을 대체하는 dangerouslySetInnerHTML은 사용에 주의가 필요합니다. 이름에서도 알 수 있듯이 사용에 위험이 따르지만 sanitize를 통해서 리액트에서 렌더링 될 때 예기치 않은 스크립트가 실행되지 않도록 사용해야 합니다.

profile
주먹펴고 일어서서 코딩해

0개의 댓글