React の dangerouslySetInnerHTML による XSS

React の dangerouslySetInnerHTML による XSS

説明

React の dangerouslySetInnerHTML は、渡された HTML をそのまま DOM に描画します。ユーザー入力や信頼できない HTML をサニタイズせずに __html に渡すと、危険なイベントハンドラーや悪意のあるリンクなどが挿入され、XSS につながるおそれがあります。

想定される影響

  • セッションやトークンの窃取、DOM の改ざんが起こる可能性があります。
  • ユーザーのブラウザーで悪意のあるスクリプトが実行され、追加の API 呼び出しやフィッシング画面の挿入につながるおそれがあります。
  • CMS、Markdown 変換、コメント描画など、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 が発生するおそれがあります。
  • 変更後: 描画する前に DOMPurify で HTML をサニタイズし、スクリプトや危険な属性を取り除きます。

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 のサニタイズと同じものとして扱わないでください。マークアップが不要なら、通常の JSX で {html} を出力する方法が簡単です。

参考資料