以前、WordPressで投稿のタイトル前にSVGアイコンを表示するプラグインを開発しました。
ところが、透明感のある要素がうまく表示されない問題に気づきました。


プレビュー画面では半透明処理がきれいに表示されるのに、実際のページでは透明度が反映されていないのです。
- WordPressでSVGアイキャッチを表示するプラグインを開発した(svg-eyecatch-iconプラグイン) – Chiilabo Note
- WordPressプラグインで追加したタイトル用のSVGコードがSNS共有時に混入していた(svg-eyecatch-iconプラグイン、Ultimate Blocks) – Chiilabo Note
1. 問題の発見
プラグインの動作確認をしていると、不思議な現象に気づきました。
管理画面のプレビュータブでは、SVGアイコンのopacity属性が正しく反映されて半透明になっています。
しかし、実際の投稿ページを見ると、不透明なままでした。
最初は「表示の仕組みが違うのかな」と思いましたが、よく考えると同じデータベースから取得したSVGコードのはずです。
なぜプレビューとページで違う結果になったのでしょうか。
2. 原因の調査
コードを読み進めると、SVGの処理が3つの場所で行われていることがわかりました。
- 1つ目は、管理画面のプレビュー表示です。
ここでは投稿者が入力したSVGコードをesc_textarea()でエスケープして表示していました。
つまり、入力したままの状態です。 - 2つ目は、データベースへの保存処理です。
ここに重要な処理がありました。wp_kses()という関数を使って、SVGコードをサニタイズしていたのです。 - 3つ目は、実際のページでの表示です。
データベースから取得したSVGコードをそのまま出力していました。
この流れを整理すると、問題の本質が見えてきます。
3. wp_ksesのサニタイズの仕組み
wp_kses()は、WordPressのセキュリティ機能の1つです。
許可されたHTMLタグと属性だけを残し、それ以外を削除します。
これをホワイトリスト方式と呼びます。
プラグインではget_allowed_svg_tags()というメソッドで、使用を許可するSVGの要素と属性を定義していました。
例えばpath要素には、こんな属性を許可していました。
'path' => array(
'd' => array(),
'fill' => array(),
'stroke' => array(),
'stroke-width' => array(),
// ...
),Code language: PHP (php)
しかし、この一覧にopacityが含まれていませんでした。
つまり、保存時にopacity属性が削除されていたのです。
プレビューは入力値をそのまま表示するためopacityが見えます。
でもデータベースに保存された時点でopacityは消えています。
だからページでは反映されなかったわけです。
4. 最初の解決案とその問題点
「それならopacityを許可リストに追加すればいい」と考えました。
実際、各SVG要素の定義に'opacity' => array(),を追加していきました。
'path' => array(
'd' => array(),
'fill' => array(),
'stroke' => array(),
'opacity' => array(), // 追加
// ...
),Code language: PHP (php)
しかし、すぐに別の疑問が浮かびます。opacity以外にも、fill-opacity、stroke-opacity、fill-ruleなど、SVGには多くの属性があります。
さらにclipPath、mask、patternといった要素も抜けていました。
全部を手動で追加していくのは大変です。
しかも、新しいSVG仕様が出たら、また追加作業が必要になります。
5. 発想の転換
ここで考え方を変えてみました。
本当に必要なのは「使えるものを列挙すること」でしょうか。
むしろ「危険なものを除外すること」が本質ではないでしょうか。
セキュリティの観点で危険なのは、主にこれらです。
<script>タグ(JavaScriptの実行)onclickなどのイベントハンドラ(JavaScriptの実行)javascript:プロトコル(JavaScriptの実行)<iframe>や<object>(外部コンテンツの埋め込み)
逆に言えば、これらさえブロックできれば、他のSVG要素や属性は自由に使えるはずです。
これをブラックリスト方式と呼びます。
6. ブラックリスト方式への変更
validate_svg()メソッドを強化して、危険な要素だけをチェックするようにしました。
// 危険なタグをチェック
$dangerous_tags = array('script', 'iframe', 'object', 'embed', 'link', 'meta', 'base');
foreach ($dangerous_tags as $tag) {
if (preg_match('/<' . $tag . '[^>]*>/i', $svg_code)) {
return array('valid' => false, 'message' => $tag . 'タグは許可されていません。');
}
}
// 危険な属性をチェック
$dangerous_attrs = array('onload', 'onerror', 'onclick', 'onmouseover', /* ... */);
foreach ($dangerous_attrs as $attr) {
if (preg_match('/' . $attr . '\s*=/i', $svg_code)) {
return array('valid' => false, 'message' => $attr . '属性は許可されていません。');
}
}
// 危険なプロトコルをチェック
if (preg_match('/javascript:/i', $svg_code)) {
return array('valid' => false, 'message' => 'javascript:プロトコルは許可されていません。');
}Code language: PHP (php)
この検証を通過したSVGコードは、そのままデータベースに保存します。wp_kses()は使いません。
// 検証を通過したSVGをそのまま保存
update_post_meta($post_id, '_svg_eyecatch_code', $svg_code);Code language: PHP (php)
プレビュー表示も同様に変更しました。
検証済みのSVGコードはそのまま表示します。
7. 不要なコードの削除
ホワイトリスト方式を廃止したことで、get_allowed_svg_tags()メソッドは不要になりました。
200行以上あった要素と属性の定義を、まるごと削除できました。
コードがシンプルになり、メンテナンスも楽になります。
8. 結果の確認
修正後、改めて動作を確認しました。
管理画面でopacity="0.5"を含むSVGコードを入力すると、プレビューで半透明に表示されます。
投稿を保存してページを表示すると、こちらも同じように半透明になりました。
データベースに保存されたSVGコードを確認すると、opacity属性がそのまま残っています。
他にもfill-opacity、stroke-opacity、clipPathなどを試しましたが、すべて正しく動作しました。
新しいSVG属性を使っても、追加の設定は不要です。
9. セキュリティの検証
安全性も確認しておきます。
<script>タグを含むSVGコードを入力すると、保存時にエラーメッセージが表示されました。
「scriptタグは許可されていません。」というメッセージです。
onclick属性やjavascript:プロトコルも、同様にブロックされました。
必要な安全性は保ちつつ、SVGの柔軟性も確保できています。
10. 学んだこと
今回の開発を通じて、セキュリティ対策における2つのアプローチを体験しました。
- ホワイトリスト方式は、安全だとわかっているものだけを許可します。確実ですが、すべてを列挙する必要があります。
- ブラックリスト方式は、危険だとわかっているものだけを禁止します。柔軟ですが、見落としがあると危険です。
どちらが優れているということではありません。状況に応じて使い分けることが大切です。
SVGのような標準化された技術では、危険な要素が限定的です。そのため、ブラックリスト方式が適していました。
一方、ユーザー入力を完全に自由にする場合は、ホワイトリスト方式の方が安全でしょう。
11. おわりに
属性が消える原因は、ホワイトトリスト方式のサニタイズにありました。ブラックリスト方式に切り替えることで、すべてのSVG機能が使えるようになりました。
この変更により、プラグインは以下の点で改善されました。
- すべてのSVG属性と要素に対応
- 新しいSVG仕様にも自動対応
- コードが200行以上削減されメンテナンスが容易に
同じような問題に直面している方の参考になれば幸いです。