dangerouslySetInnerHTMLっていかにも危険?!

dangerouslySetInnerHTMLが必要なった場面

Reactコンポーネントでこんなコードを書きました。

const mediaContent = getMediaContent();
return(
    <div className="post_text">
        { mediaContent }
    </div>
)

getMediaContent()は画像等を表示するためのHTMLを生成するためのオリジナルの関数です。mediaContentにはHTMLの文字列が格納されます。 これで画像が表示されるかというと、さにあらず。文字列がブラウジングされずに表示されました。 なんでなのと思って、ChatGPTに聞きました。

JavaScript (JSX) では、HTML 文字列を直接レンダリングすると、文字列として表示されてしまいます。文字列を HTML としてレンダリングするには、dangerouslySetInnerHTML プロパティを使用する必要があります。」 コードは次のようになります。

const mediaContent = getMediaContent();
return(
     <div
        className="post_text"
        dangerouslySetInnerHTML={{ __html: mediaContent }}
    />
)

これで、きちんとブラウジングされました。しかし、なんだかいかがわしい名前の属性と思ってChatGPTの回答を読んでいると、

dangerouslySetInnerHTML は名前の通り、セキュリティリスクがあるため注意して使用してください。mainText に不正なスクリプトが含まれている場合、クロスサイトスクリプティング (XSS) 攻撃の対象となる可能性があります。

まあ、getMediaContent()の出元がはっきりしていればいいようです。

不要なタグが付いてしまうという問題

実は話はこれで終わりません。ここからが備忘録として記録しておくべきところ。 このdangerouslySetInnerHTMLは属性なので、何らかのタグとともにでないと使えません。つまりmediaContentで出来上がった文字列が必要なタグを全部備えていると、不要なタグが一つつくことになるのです。 この不要なタグは実は問題です。mediaContentで出来上がるDOM要素が場面によって変化してしまうのです。これではCSSだって当たらなくなる可能性もあるのです。これは一大事です。

ちなみにWordpressが用意しているwp.element.Fragmentをインポートして

<Fragment 
    dangerouslySetInnerHTML={{ __html: mediaContent }} 
/>

としてもダメです。全く表示されなくなりました。

html-react-parserライブラリの活用

でも、回避する方法はありました。 html-react-parser というライブラリがあるのです。 まず、次のコマンドでインストールします。

npm install html-react-parser

それでReactコンポーネントには次のように書くとOKです。とってもすっきりしてるじゃないですか。

import parse from 'html-react-parser';

// ...

return (
  <>
    {parse(mediaItem)}
  </>
);

ということでhtml-react-parserを使うのがベストチョイスと思いました。だたし、この方法でも

信頼できない HTML 文字列が含まれる場合のセキュリティリスクに注意してください。必要に応じて、サニタイズ処理を実施してください。 -- ChatGPTからの忠告

JSXで要素を直接作成する

でも、条件によってはもっと簡単な方法がありました。 それはJSXで要素を直接作成するという方法です。HTMLを生成している関数が複雑だとそういうわけにはいかないのですが、たとえば、flgという状態変数がtrueなら文字列にspanタグをつけて表示したいというような場合です。 まずは、こんなコードを試してみます。

const dispLabel = flg ? `${labelContent}<span>(${required.display})<span>` : labelContent;
return(
    <label>
        {dispLabel}
    </label>
)

テンプレートリテラルで文字列を生成してレンダリングしようとしています。これだと

WordPr.png

となります。そこで

const dispLabel = flg ? `${labelContent}<span>(${required.display})<span>` : labelContent;
return(
    <label
        dangerouslySetInnerHTML={{ __html: dispLabel }}
    />
)

とすればうまくいくのですが、そもそもdispLabel を文字列で生成するのがよくないわけです。 そこで、

const dispLabel = required.flg ? <>{labelContent}<span>({required.display})</span></> : labelContent;
return(
    <label>
        {dispLabel}
    </label>
)

これでうまくいくのです。 少し説明を加えます。 JSX(JavaScript XML)は、Reactの一部として提供される拡張構文で、JavaScriptの中にHTMLタグを書けるようになるという特徴があります。

const element = <h1>Hello, world!</h1>;

と書くと、、elementはReactが理解できる特別なオブジェクトになります。これはJavaScriptのオブジェクト表現となり、Reactはこれを利用してDOMを更新します。これを「Reactエレメント」と呼びます。 つまり、文字列ではなく「Reactエレメント」をつくればいいということです。 これでdangerouslySetInnerHTMLを使わずに済む範囲が大きくなるのではないでしょうか。