「API には target="_blank" が入ってるのに、なんで別タブで開かないんだろう???」
今回、microCMS で管理しているお知らせ本文の中で、外部ファイルへのリンクが想定どおり別タブで開かないことがありました。
最初は microCMS 側の設定や出力を疑ったのですが、実際に見ていくと原因はそこではなく、フロント側で使っていた DOMPurify のサニタイズ処理にありました。
今回はその備忘録として、
- 調査
- 原因
- 解決
の流れでまとめておきます。
調査:どこで target が消えているのか見てみた
やっていること自体はシンプルです。
- microCMS のリッチエディタで本文を書く
- 本文の中に外部リンクを入れる
- フロント側で HTML を受け取って表示する
見た目としては「外部リンクが別タブで開かない」だけだったのですが、こういうときって最初に「どこで値が消えたのか」を切り分けるのが大事だなと思いました。
実際に確認したのはこの3つです。
- microCMS に保存されている本文
- API レスポンスで返ってくる HTML
- 最終的に画面に出している HTML
調べてみると、microCMS 側にも API レスポンスにも target="_blank" は入っていました。
つまり、「CMS に保存されていない」とか「API で返ってきていない」という話ではなかったです。
じゃあどこで消えたのかというと、表示前にかけていたサニタイズ処理のあとでした。
原因:落ちていたのはtarget だった件
犯人は DOMPurify でした。
フロント側で セキュリティ対策のライブラリとしてDOMPurify を使用して、HTML等の表示をしていました。
これ自体は正しいと思っています。
ただ、DOMPurify は安全寄りのデフォルト設定を持っていて、許可リストにないタグや属性は暗黙的に除去されることに対する理解が足りなかったです。
正直それまでは、DOMPurify を
「危ないものをいい感じに削ってくれる便利なやつ」
くらいの感覚で見ていたのも原因でした。
公式の情報を改めて見てみると、DOMPurify は allow-list ベースで動いていて、許可リストに入っていないタグや属性は暗黙的に削除されます。
さらに、必要なら FORBID_TAGS や FORBID_ATTR で明示的な block-list も設定できます。
今回のケースに当てはめると、こちらとしては
「外部リンクだから別タブで開いてほしい」
という要件がありました。
でも DOMPurify 側から見ると、target は
まだ通してよいと決まっていない属性
だったみたいなんですよね。
つまり、何かがおかしくて壊していたというより、
未許可の属性を安全のために落としていた
という見方のほうが正しかったです。
今回あらためて整理してみると、実際には
何を通してよくて、何を通さないかを allow-list で決める仕組み
として理解したほうがよかったという結果になります。
原因を見て、セキュリティ的にも少し考えた
今回 target="_blank" を通すにあたって、セキュリティ面も少し考えてみました。
この文脈でよく出てくるのが Reverse Tabnabbing です。
これは、新しいタブで開いた先のページが元ページを window.opener 経由で操作して、元のタブを別サイトに書き換えるような攻撃のことです。
現在の主要ブラウザでは、<a target="_blank"> は rel="noopener" 相当の挙動になる案内が MDN にもあり、以前ほど「必ず明示しないと危ない」という話ではなくなっているようです。
一方で、rel="noopener" を明示しておくこと自体に意味がなくなったわけでもないと思っています。
たとえば、
- コードを見たときに意図がわかりやすい
- 「安全に別タブを開く」という方針が残る
- レビュー時にも判断しやすい
といった実務上のメリットはあります。
なお noreferrer については、参照元を送らない意味もあるので、解析や流入確認への影響まで含めて要件次第かなと思いました。
このへんも含めて、リンクって href があれば終わりではなくて、
どう開くか、どこまで情報を渡すか
まで含めて設計なんだなと改めて感じました。
解決:必要な属性を、理由を持って許可した
修正自体はシンプルでした。
a タグに対して、href だけでなく targetも許可するようにしました。
import DOMPurify from 'dompurify';
const cleanHtml = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_ATTR: ['href', 'target'],
});
実際のプロジェクトでは、タグ全体の許可設定や他の属性との兼ね合いもあると思うので、このままではないかもしれません。
ただ考え方としては同じで、動かないから雑に緩めるのではなく、必要な属性を、理由をもって allow-list に追加するという流れです。
まとめ
今回の件は、表面的には
「外部リンクが別タブで開かなかった」
というだけの小さな不具合でした。
でも実際には、
- microCMS は HTML をちゃんと返していた
- API にも
target="_blank"は入っていた - 表示前の DOMPurify で属性が落ちていた
という流れで、原因はかなり素直でした。
microCMS の問題ではなく、こちらが安全のために入れていたサニタイズ設定の話だったわけです。
個人的に一番大きかったのは、不具合を直せたことそのものより、ここで一回ちゃんと立ち止まれたことでした。
今までは DOMPurify を
「危ないものをなんとなく消してくれるライブラリ」
として見ていたところがあったのですが、今回は
何を通してよくて、何を通さないのかをこちらが決めるための仕組み
として考えられたのが大きかったです。
小さいハマりどころではあったのですが、CMS 由来の HTML を扱うときに
見た目だけではなく、どのタグや属性を、どんな理由で許可するのか
を意識するきっかけになりました。
同じようなところで引っかかったときの、自分用の備忘録として残しておきます。
採用拡大中!
アシストエンジニアリングでは一緒に働くフロントエンド、バックエンドのエンジニア仲間を大募集しています!
少しでも興味ある方は、カジュアル面談からでもぜひお気軽にお話ししましょう!
お問い合わせはこちらから↓
https://official.assisteng.co.jp/contact/