💻 Frontend

dangerouslySetInnerHTML

category
💻 Frontend

dangerouslySetInnerHTML

과제를 하면서 dangerouslySetInnerHTML 라는 것을 사용하게 됐는데, 문제점을 발견하고 사용하지 않게 되었다. 이번 포스팅을 통해 dangerouslySetInnerHTML은 어떤 기능을 갖고 있고, 어떠한 문제에 도달하게 되었는지 정리해 보려고 한다!
 

DOM

js에서 getElementById을 통해 요소를 가져오고 직접 DOM을 조작하는 경우가 많죠? 아래와 같은 코드들을 많이 작성하셨을 겁니다.
// JS const element = document.getElementById('content'); element.innerHTML = "<div style='color:red'>A</div>";
 
하지만 React에서는 직접 DOM을 조작하지 않고도 요소를 삽입할 수 있습니다.
// JSX(Recat) const element = 'A'; <div>{element}<div>
 
그런데 React에서 DOM을 설정하는 기능 중 innerHTML을 사용하고 싶다면 어떨까요?

innerHTML

React 공식 문서에서도 직접 말하길, dangerouslySetInnerHTMLinnerHTML을 사용하는 React의 대체 방법이라고 합니다. 일반적으로 코드에서 HTML을 설정하는 것은 사이트 간 스크립팅 공격에 쉽게 노출될 수 있기 때문에 위험합니다.
 
따라서 React에서 직접 HTML을 설정할 순 있지만, 위험하다는 것을 상기시키기 위해 dangerouslySetInnerHTML라는 대체 방법이 나타난 것입니다.
function MyComponent() { return <div dangerouslySetInnerHTML={{__html: 'First &middot; Second'}} />; }
 
그렇다면 React 공식 문서에서 말하는 사이트 간 스크립트 공격이 무엇일까요?

사이트 간 스크립팅 공격(XSS)

XSS는 웹 페이지에 악성 스크립트를 삽입하여 의도치 않은 동작을 실행하는 공격을 말합니다. 주로 여러 사용자가 보는 게시판에 악성 스크립트가 담긴 글을 올리는 형태로 이루어집니다.
 
예를 들어 해커가 alert가 100개 있는 script를 삽입하면 어떻게 될까요? 일반 사용자는 alert 100개를 다 넘겨야 게시글을 확인할 수 있습니다.
<script>alert('XSS 공격 발생!!!') * 100 </script>
 
또 다른 예시를 들어보죠.
<img src="에러 발생 시키기!" onerror="악성 페이지로 이동 하기!" />
이미지 src에 악의적으로 에러를 발생시키게 없는 주소를 넘기고, onerror를 실행시켜 악성 페이지로 이동시키게 할 수 도 있습니다.
 
정말 치명적인 취약점인데요. 저도 dangerouslySetInnerHTML을 사용해 개발하던 도중 XSS 위험에 노출된 경험이 있습니다.
 

발생한 예제

과제를 하던 도중 api response 값에 HTML Entity가 포함되어 있었습니다. 특수문자 따옴표가 HTML Entity로 인코드 되어 있어 &#34; 이런 식으로 변환되었습니다.
notion image
 
그래서 해당 HTML Entity를 디코드시키기 위해 dangerouslySetInnerHTML로 텍스트를 표시하였는데요, HTML Entity는 처리 되었지만 XSS 취약점이 발견되었습니다.
아래는 api response 값에 img 태그를 삽입한 결과입니다. 문자열 그대로 보이는 것이 아닌 태그가 변한되어 이런 현상이 발생했습니다.
notion image
 

해결 방법

dangerouslySetInnerHTML XSS 취약점을 해결하기 위해 현재는 많은 방법이 있습니다.
  • DOM Purify (외부 라이브러리)
  • HTML Sanitizer API (브라우저에서 자체적으로 제공)
여러 방법이 있지만 저는 직접 구현하는 방법을 택했습니다. 참고자료 →
  1. 가상 div를 생성한다
  1. 해당 요소의 innerHTML로 string을 넘겨준다
  1. 2번 과정에서 HTML 엔티티가 변환된다.
  1. 변환된 text를 가져온다. (element.textContent)
const decodeHTMLEntities = (str: string): string => { const element = document.createElement("div"); element.innerHTML = str; return element.textContent ?? ""; };
직접 페이지에 보여지는 DOM을 설정하는 것이 아닌 가상 element를 생성하기 때문에 웹 페이지 상에서 XSS 취약점에 노출될 일이 없습니다.
 

마무리

DOM을 직접 조작하는 것은 매운 위험한 것입니다. React도 그것을 인지하고 있어서 dangerouslySetInnerHTML 라는 대체 방법이 있지만 최대한 사용을 지양하라고 하였습니다.
저는 편리하게 HTML Entity를 변환시켜 주어 아무 걱정 없이 사용하였는데, 항상 왜라는 사고로 어떻게 동작하는지 생각하면서 개발해야겠다고 느꼈습니다.
 

References