React dangerouslySetInnerHTML 기반 XSS

React dangerouslySetInnerHTML 기반 XSS

설명

React의 dangerouslySetInnerHTML는 전달된 HTML을 그대로 DOM에 렌더링합니다. 사용자 입력이나 신뢰되지 않은 HTML을 정화 없이 __html에 전달하면 위험한 이벤트 핸들러나 악성 링크 등이 삽입되어 XSS로 이어질 수 있습니다.

잠재적 영향

  • 세션 탈취, 토큰 탈취, DOM 변조가 발생할 수 있습니다.
  • 악성 스크립트가 사용자 브라우저에서 실행되어 추가 API 호출이나 피싱 UI 삽입이 가능해집니다.
  • CMS, 마크다운 변환, 댓글 렌더링 같은 HTML 출력 기능 전반이 공격면이 됩니다.

해결 방법

  • dangerouslySetInnerHTML 사용을 최소화하고, 가능하면 일반 JSX 출력으로 대체하세요.
  • HTML 렌더링이 필요하면 DOMPurify 같은 검증된 HTML 정화 도구로 처리한 결과만 사용하세요.
  • 사용자 입력을 직접 __html에 넣지 마세요.

예시

변경 전

javascript
import React from "react";

export function Bad({ html }) {
  return <section dangerouslySetInnerHTML={{ __html: html }} />;
}

변경 후

javascript
import React from "react";
import DOMPurify from "dompurify";

export function Good({ html }) {
  return <section dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(html) }} />;
}

설명:

  • 변경 전: 사용자 입력을 정화 없이 dangerouslySetInnerHTML에 전달하므로 XSS가 발생할 수 있습니다.
  • 변경 후: HTML을 렌더링하기 전에 DOMPurify로 정화해 스크립트나 위험한 속성을 제거합니다.

striptags 옵션 경계

javascript
import React from "react";
import stripTags from "striptags";

export function UnsafeAllowedTags({ html }) {
  return <section dangerouslySetInnerHTML={{ __html: stripTags(html, "<img>") }} />;
}

export function UnsafeReplacement({ html }) {
  return <section dangerouslySetInnerHTML={{ __html: stripTags(html, [], "<img src=x onerror=alert(1)>") }} />;
}

export function DefaultStripping({ html }) {
  return <section dangerouslySetInnerHTML={{ __html: stripTags(html) }} />;
}

태그를 허용하면 위험한 속성까지 남을 수 있고, 대체 문자열로 HTML을 다시 삽입할 수도 있습니다. 태그 제거를 HTML 정화와 같은 것으로 취급하지 마세요. HTML을 보존할 필요가 없다면 일반 JSX의 {html} 출력이 더 단순합니다.

참조