react-markdown 원시 HTML 렌더링으로 인한 XSS

react-markdown 원시 HTML 렌더링으로 인한 XSS

설명

React 환경에서 react-markdown을 사용할 때 신뢰되지 않은 Markdown에 원시 HTML 렌더링을 허용하면 XSS(크로스 사이트 스크립팅)에 노출될 수 있습니다. 최신 react-markdown은 기본적으로 HTML을 이스케이프하거나 건너뛰지만, rehype-raw로 HTML 파싱을 켜고 rehype-sanitize 같은 정화 단계를 적용하지 않으면 공격자가 악성 태그나 속성을 삽입할 수 있습니다. 오래된 버전에서는 allowDangerousHtml 또는 escapeHtml={false} 같은 옵션이 같은 위험을 만들 수 있습니다.

잠재적 영향

  • 사용자 정보 탈취: 공격자가 사용자의 쿠키, 세션, 또는 기타 민감 정보를 획득할 수 있습니다.
  • 악성 스크립트 실행: 사용자의 브라우저에서 악성 코드가 실행되어 추가적인 공격이 이뤄질 수 있습니다.
  • 사이트 신뢰도 하락: 악성 코드로 인해 사용자가 사이트를 신뢰하지 않게 됩니다.

해결 방법

  • 신뢰되지 않은 Markdown에는 rehype-raw를 사용하지 않고 기본 렌더링을 유지하세요.
  • 원시 HTML을 반드시 허용해야 한다면 rehype-raw 뒤에 rehype-sanitize를 적용하고 허용 태그·속성 스키마를 최소화하세요.
  • 사용자 입력(markdown 및 렌더링되는 데이터)에 추가적인 검증과 정화를 적용하세요.
  • URL 변환 옵션을 직접 오버라이드할 경우 javascript:, data: 등 위험한 스킴을 차단하세요.

예시

변경 전

tsx
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";

function UnsafeComponent({ markdown }) {
  // 취약: 신뢰되지 않은 Markdown의 원시 HTML을 그대로 파싱
  return <ReactMarkdown rehypePlugins={[rehypeRaw]}>{markdown}</ReactMarkdown>;
}

변경 후

tsx
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";

function SafeComponent({ markdown }) {
  // 가장 안전한 기본값: 원시 HTML을 파싱하지 않음
  return <ReactMarkdown>{markdown}</ReactMarkdown>;
}

function SafeHtmlComponent({ trustedButNeedsHtml }) {
  // HTML 허용이 필요할 때도 정화 단계를 함께 적용
  return (
    <ReactMarkdown rehypePlugins={[rehypeRaw, rehypeSanitize]}>
      {trustedButNeedsHtml}
    </ReactMarkdown>
  );
}

설명:

  • 변경 전: rehype-raw를 단독으로 사용하면 Markdown 안의 원시 HTML이 React 요소로 변환되어, 정화되지 않은 HTML이 실행 가능한 DOM으로 들어갈 수 있습니다.
  • 변경 후: 기본 렌더링은 원시 HTML을 실행하지 않습니다. HTML 렌더링이 필요한 경우에도 rehype-sanitize로 허용된 태그와 속성만 남겨 XSS 가능성을 줄입니다.

참조