Blog

innerHTML을 사용할 때 조심해야 하는 이유


서론#

JavaScript를 학습하면서 DOM을 직접 다루는 코드가 필요해 innerHTML을 사용했다.
처음에는 잘 동작했고, 크게 문제도 없어 보였다.

그런데 innerHTML은 사용에 주의해야 한다는 이야기를 자주 보게 됐고,
왜 그렇게까지 경계하는지 궁금해졌다.
이 글은 그 이유를 정리한 기록이다.

innerHTML이란?#

innerHTML은 요소 내부에 문자열을 넣으면,
그 문자열을 HTML로 해석해 기존 내용을 제거하고 DOM 구조를 다시 만드는 속성이다.

문자열만 넘기면 바로 화면을 구성할 수 있어서 편리하지만,
브라우저는 이 문자열이 어디서 왔는지까지는 판단하지 않는다.

innerHTML 사용 예제#

const div = document.createElement("div");
div.innerHTML = "<p>Hello <strong>World</strong></p>";

console.log(div.innerHTML);
// <p>Hello <strong>World</strong></p>

innerHTML의 문제점#

문제는 사용자 입력이 섞이는 순간이다.

const userInput = "<img src=x onerror=alert('XSS') />";
result.innerHTML = userInput;

이때 onerror는 이미지 로딩 실패 시 실행되는 이벤트 핸들러다.

이 경우, 브라우저는 문자열을 그대로 HTML로 해석하고
onerror에 들어 있는 스크립트까지 실행한다.

이처럼 사용자 입력을 그대로 해석하게 되면,
개발자가 의도하지 않은 스크립트가 실행될 수 있다.

XSS란?#

이런 방식으로 사용자 입력을 통해 스크립트가 실행되는 취약점을
XSS(Cross-Site Scripting)라고 부른다.

중요한 건 용어 자체보다도,
의도하지 않은 코드가 실행될 수 있다는 점이다.

가장 간단한 방지 방법#

HTML 구조가 필요 없는 경우라면
애초에 HTML로 해석되지 않게 만드는 것이 가장 안전하다.

result.textContent = userInput;

textContent는 태그를 문자열 그대로 취급하기 때문에
스크립트가 실행되지 않는다.

React에서는 왜 덜 보이는가?#

React는 JSX에 문자열을 렌더링할 때
기본적으로 HTML을 이스케이프 처리한다.

이 보호는 React가 대신 해주는 것이지,
브라우저의 innerHTML이 안전해진 것은 아니다.

<div>{userInput}</div>

이 방식에서는 문자열이 그대로 텍스트로 출력되기 때문에
innerHTML처럼 바로 XSS로 이어지지 않는다.

다만 다음과 같은 코드는 예외다.

<div dangerouslySetInnerHTML={{ __html: userInput }} />

이 경우에는 innerHTML과 동일한 위험을 갖는다.

sanitize란?#

sanitize는 입력값에서 위험한 태그나 속성을 제거해
안전한 HTML만 남기는 과정을 말한다.

innerHTML을 반드시 사용해야 하는 상황이라면
sanitize된 결과만 넣어야 한다.

대표적인 라이브러리로 DOMPurify가 있다.

import DOMPurify from "dompurify";

const userInput = "<img src=x onerror=alert('XSS') />";
const clean = DOMPurify.sanitize(userInput);

result.innerHTML = clean;

sanitize는 위험을 줄여주지만,
잘못된 설정이나 검증되지 않은 라이브러리 사용 시에는 여전히 취약할 수 있다.

그럼 언제 써도 되는가?#

다음과 같은 경우에는 상대적으로 안전하다.

  • 서버에서 직접 생성한 HTML
  • 마크다운을 파싱한 결과 (신뢰 가능한 파서와 sanitize를 거친 경우)
  • 사용자 입력이 전혀 없는 정적 콘텐츠

반대로, 사용자가 만들 수 있는 값이 섞이면 사용하지 않는 것이 원칙이다.

정리#

innerHTML은 위험해서 쓰면 안 되는 API가 아니다.
다만 사용할 수 있는 조건이 매우 까다로운 API다.

사용자 입력이 섞이는 순간,
그 책임은 전부 개발자에게 넘어온다.

5