본문으로 건너뛰기
개발 머꼬
개발 노트HTML
hohyeon.dev23

사용자 글에 dangerouslySetInnerHTML을 썼다가 스크립트가 실행된 사고

  • #Engineering Note
  • #React
  • #Security
  • #XSS

문제 발생

사용자가 작성한 게시글을 HTML 서식(굵게, 링크 등)까지 표시하려고 dangerouslySetInnerHTML로 그대로 렌더링했는데, 한 사용자가 게시글에 <script> 태그를 심어 다른 방문자의 브라우저에서 임의 스크립트가 실행되는 문제가 발생했습니다.

function PostContent({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />; // 사용자 입력을 그대로 삽입
}

원인 분석

이건 **저장형 XSS(Stored Cross-Site Scripting)**의 전형적인 사례입니다. 브라우저는 innerHTML에 들어간 내용을 실제 HTML로 파싱합니다 — 그 안에 <script> 태그나 onerror="..." 같은 이벤트 핸들러 속성이 있으면, 그것도 똑같이 실행 가능한 코드로 취급합니다.

공격자가 자신의 게시글에 악성 스크립트를 심어두면, 그 글을 읽는 모든 다른 사용자의 브라우저에서 그 스크립트가 실행됩니다 — 로그인 세션 쿠키 탈취, 사용자 몰래 다른 요청 전송, 피싱 페이지로 리다이렉트 등 다양한 공격으로 이어질 수 있습니다. 공격자 본인이 아니라 그 콘텐츠를 소비하는 다른 사용자가 피해자가 된다는 점에서 특히 위험합니다.

해결 방안

  1. 가능하면 애초에 dangerouslySetInnerHTML을 쓰지 않습니다. React는 기본적으로 텍스트를 문자열로 렌더링할 때 자동으로 이스케이프 처리하므로, 서식이 필요 없는 일반 텍스트라면 그냥 {content}로 렌더링하는 것이 가장 안전합니다.
  2. 서식(굵게, 링크, 목록 등)이 정말 필요하다면, 사용자 입력을 Markdown 같은 안전한 서식 언어로 받고, 실제 렌더링은 dangerouslySetInnerHTML이 아니라 안전한 마크다운 렌더러(예: React 엘리먼트 트리를 직접 만드는 라이브러리)로 처리합니다 — 이 방식은 애초에 원본 사용자 입력이 HTML로 파싱되는 경로 자체가 없어 XSS 표면이 원천적으로 없습니다.
  3. 불가피하게 사용자 입력을 HTML로 렌더링해야 하는 경우(리치 텍스트 에디터 등)라면, 반드시 DOMPurify 같은 검증된 sanitizer 라이브러리로 위험한 태그/속성(<script>, on* 이벤트 핸들러, javascript: URL 등)을 제거한 뒤에만 dangerouslySetInnerHTML에 넣습니다.
import DOMPurify from "dompurify";

function PostContent({ html }) {
  const clean = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
  1. 직접 정규식으로 <script> 태그만 걸러내는 방식은 충분하지 않습니다 — 이벤트 핸들러 속성, javascript: 프로토콜, 인코딩 우회 등 회피 기법이 매우 다양해서, 검증되지 않은 자체 필터링은 뚫릴 가능성이 높습니다. 반드시 검증된 sanitizer 라이브러리를 씁니다.
  2. JSON-LD 같은 HTML이 아닌 데이터를 스크립트 태그 안에 넣는 경우도 별개로 주의가 필요합니다 — 그 경우엔 sanitize가 아니라 </script> 시퀀스 자체를 이스케이프하는 다른 방어가 필요합니다(용도가 다른 문제입니다).

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.