HTML Sanitizer API는 2021년 11월,
XSS(cross-site-scripting)을 방지하고 안전하게 DOM 데이터를 관리할 수 있도록 하기 위해 소개된 기술이다.
XSS 공격이란?
신뢰할 수 없는 외부로부터의 데이터가 href 경로, 또는 innerHTML 속성 등 페이지 DOM에 삽입되는 경우가 있다. 특히 사이트에 스크립트 코드를 삽입하게 되면, DOM 트리에 접근할 수 있고 쿠키와 세션 정보에도 접근이 가능해진다. 사용자가 페이지를 사용할 때 미리 심어놓은 스크립트가 실행되는 원리.
보통 사용자가 입력하는 인풋이나, 에디터를 직접 구현한 경우에
그 안에 어떤 코드가 들어갈지 정확히 예측할 수 없다는 리스크가 존재한다.
XSS를 통해 사용자가 페이지에 진입하면 계속해서 브라우저 알럿이 뜨게 할 수도 있고,
쿠키 정보에 접근해 url로 전송하는 것도 가능해진다.
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`;
div.innerHTML = user_input;
이런 XSS를 방어하기 위한 방법으로는
사용자 입력이 필요할 경우 클라이언트 또는 서버에서 유효성 체크를 하거나,
innerHTML
속성이 아닌 textContent
속성을 이용하여 마크업이 아닌 이스케이프 처리된 텍스트로 코드를 처리하는 방법이 주로 사용된다.
예를 들어, & -> &
, < -> <
, > -> >
등으로 치환한 텍스트를 사용하게 되면 특정 문자나 기호가 치환되기 때문에 위험성을 줄일 수 있다.
React DOM 역시 JSX에 삽입된 모든 값으로 렌더링하기 전에 이스케이프한다. 이를 통해 HTML 태그나 스크립트 기능으로 볼 수 있는 부분이 사라지기 때문에 XSS 공격을 방지한다.
그런데 위에서 봤던 예시에서, <em>
태그는 이스케이프하지 않은 상태로 값을 관리하고 싶다면?
예를 들어 태그에 특정 속성, 또는 스타일이 적용되었다고 한다면 이는 유지되어야 할 수도 있다.
이럴 경우에 sanitizing을 사용하면 된다.
sanitizing은 위험 요소가 될만한 부분을 자체적으로 삭제하여 (예를 들면 스크립트와 같은)
안전성을 보장하는 방법이다.
위와 비슷한 코드로 예를 들어 보자.
// XSS 🧨
$div.innerHTML = `<em>hello world</em><img src="" onerror=alert(0)>`
// Sanitized ⛑
$div.innerHTML = `<em>hello world</em><img src="">`
sanitized 되고 나면 onerror 부분이 제거된다.
sanitizer API는 인풋에 입력된 스트링을 HTML로 파싱하여 읽은 후, 위험 요소가 있는 태그나 속성(attribute)을 제거하고 안전한 부분만 남긴다.
sanitizer API는 아래와 같이 동작한다.
const el = document.querySelector('div')
const user_input = `<em>hello world</em><img src="" onerror=alert(0)>`
const sanitizer = new Sanitizer();
el.setHTML(user_input, sanitizer); // <div><em>hello world</em><img src=""></div>
sanitizer API를 사용하려면 생성자를 사용하여 인스턴스 Sanitizer()
를 만들고 구성한다. Element.setHTML()
메소드는 HTML 스트링을 파싱하고 위험할 수 있는 부분을 삭제한 다음
현재 요소의 자식으로 DOM에 삽입한다.
setHTML(
input, // 검증할 스트링 값
sanitizier // Sanitizer 객체
);
(MDN 문서에서는 안전성을 위해setHTML
을 Element.innerHTML
대신 사용하라고 권장하고 있다)
만약 Element.setHTML()
의 형태로 사용할 수 없는 경우 - DOM의 엘리먼트가 나중에 생성되는 경우 등 - 에는 Sanitizer.sanitizeFor()
을 사용할 수도 있다.
<td>
태그를 포함한 HTML 스트링은 <table>
엘리먼트 안에서는 유효하지만,
<div>
태그 안에서 사용되는 경우에는 제거된다.
<em>
엘리먼트는 <div>
안에서는 사용 가능하지만, <textarea>
안에서 사용되면 이스케이핑 된다.
<!-- "<em>bla</em>" inserted into <div> -->
<div><em>bla</em></div>
<!-- "<em>bla</em>" inserted into <textarea> -->
<textarea><em>bla</textarea>
<script>
엘리먼트는 허용되지 않기 때문에 디폴트로 삭제된다.
const unsanitized_string = "abc <script>alert(1)</script> def";
const sanitizer = new Sanitizer();
const target = document.getElementById("target");
target.setHTML(unsanitized_string, sanitizer);
console.log(target.innerHTML);
// "abc def"
// <script>는 제거된다.
참고로 sanitizer API는 아직 experimental 단계라고 소개되어 있기 때문에
사용하려면 브라우저 지원 확인이 필요하다.
참고 :
Safe DOM manipulation with the Sanitizer API : (https://web.dev/sanitizer/#the-difference-between-escaping-and-sanitizing)
HTML Sanitizer API : (https://developer.mozilla.org/en-US/docs/Web/API/HTML_Sanitizer_API#specifications)
이미지 출처 : (https://blog.bitsrc.io/javascript-sanitizer-api-the-modern-way-to-safe-dom-manipulation-828d5ea7dca6)