2026年6月24日
2026年6月24日
WordPressのテーマでSVGをインラインで使う安全な実装方法(wp_ksesとスプライト)
テーマでSVGをインライン出力するなら、「自分で用意したSVGファイルだけを子テーマに置き、PHPで読み込んで wp_kses() で許可したタグと属性だけを残してから出力する」のが安全な基本形です。この記事では、アイコンを出力する関数の作り方、同じアイコンを何度も使うときのスプライト方式、ユーザーがアップロードしたSVGを扱うときの注意点を順に説明し、読み上げへの配慮と確認方法までまとめます。
なぜインラインSVGに注意が必要なのか
SVGは画像ファイルに見えますが、中身はXMLで書かれた文書です。<script> 要素や onload のようなイベント属性、外部リソースを参照する記述を含めることができ、HTMLに直接埋め込むとそれらがページの一部として動く可能性があります。つまり、出どころの分からないSVGをそのままインライン出力すると、クロスサイトスクリプティング(XSS)の入り口になり得ます。
一方で、インラインSVGには img タグで読み込む場合にない利点があります。
- CSSの
fill: currentColorで文字色に合わせて色を変えられる - ホバー時の色変更やアニメーションをCSSで指定できる
- アイコンごとに画像ファイルを読み込むリクエストが発生しない
利点を生かしつつ危険を避けるために、「どこから来たSVGか」と「出力前に何を取り除くか」の2点を設計に組み込みます。WordPressの初期状態ではSVGはメディアにアップロードできない設定になっており、これも同じ理由によるものです。
方法1:子テーマのSVGファイルを関数で出力する
もっとも扱いやすいのは、テーマ制作者が用意したSVGファイルを子テーマ内に置き、PHPの関数から呼び出す方式です。ファイルの置き場所は例えば assets/icons/ とし、menu.svg や search.svg のような名前で保存します。
保存するSVGは、デザインツールから書き出したままにせず、不要なメタデータやエディター固有の属性を削っておきます。fill を固定色で指定している場合は currentColor に置き換えると、CSSで色を変えられるようになります。
まず、出力を許可するタグと属性の一覧を作ります。
<?php
// 子テーマの functions.php に追記
function mychild_svg_allowed_html() {
$common = array(
'class' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'transform' => true,
);
return array(
'svg' => array_merge(
$common,
array(
'xmlns' => true,
'viewbox' => true,
'width' => true,
'height' => true,
'role' => true,
'aria-hidden' => true,
'aria-labelledby' => true,
'focusable' => true,
)
),
'title' => array( 'id' => true ),
'desc' => array( 'id' => true ),
'g' => $common,
'path' => array_merge( $common, array( 'd' => true, 'fill-rule' => true, 'clip-rule' => true ) ),
'circle' => array_merge( $common, array( 'cx' => true, 'cy' => true, 'r' => true ) ),
'rect' => array_merge( $common, array( 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'ry' => true ) ),
'line' => array_merge( $common, array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true ) ),
'polyline' => array_merge( $common, array( 'points' => true ) ),
'polygon' => array_merge( $common, array( 'points' => true ) ),
);
}
viewBox を小文字の viewbox で書いているのは、wp_kses() が属性名を小文字にして許可リストと照合するためです。大文字を含めて書くと一致せず、属性が消えてアイコンの縮尺が崩れます。ここに script、foreignObject、style、on で始まるイベント属性を加えないことが安全性の要です。
次に、アイコン名を受け取ってSVGを返す関数です。
<?php
// 子テーマの functions.php に追記
function mychild_get_icon( $name ) {
static $cache = array();
// 英小文字・数字・ハイフン・アンダースコア以外を取り除き、../ などを使えなくする
$name = sanitize_key( $name );
if ( '' === $name ) {
return '';
}
if ( ! isset( $cache[ $name ] ) ) {
$file = get_stylesheet_directory() . '/assets/icons/' . $name . '.svg';
if ( ! is_readable( $file ) ) {
return '';
}
$svg = wp_kses( file_get_contents( $file ), mychild_svg_allowed_html() );
// 装飾用アイコンとして読み上げ対象から外す
$tags = new WP_HTML_Tag_Processor( $svg );
if ( $tags->next_tag( 'svg' ) ) {
$tags->set_attribute( 'aria-hidden', 'true' );
$tags->set_attribute( 'focusable', 'false' );
$tags->add_class( 'icon' );
$tags->add_class( 'icon-' . $name );
}
$cache[ $name ] = $tags->get_updated_html();
}
return $cache[ $name ];
}
テンプレートでは次のように呼び出します。返り値は wp_kses() を通した後のものなので、そのまま echo できます。
<button type="button" class="search-toggle">
<?php echo mychild_get_icon( 'search' ); ?>
<span class="screen-reader-text">検索を開く</span>
</button>
この関数で気を付けている点は3つあります。1つ目は sanitize_key() でアイコン名を制限し、../ のような文字列で別の場所のファイルを読ませないことです。2つ目は、自前のファイルであっても wp_kses() を通すことです。外部の素材サイトから取ったSVGに想定外の要素が含まれていても、ここで取り除かれます。3つ目は static 変数によるキャッシュで、同じページで同じアイコンを何度出しても、ファイルの読み込みと整形は1回で済みます。
WP_HTML_Tag_Processor はWordPress 6.2で追加されたクラスで、HTMLの属性を文字列置換ではなく構文として安全に書き換えられます。これより古いWordPressを使っている場合は、SVGファイル側にあらかじめ aria-hidden="true" と focusable="false" を書いておき、この部分を省きます。
方法2:同じアイコンを繰り返すならスプライトにする
一覧ページのように同じアイコンを数十回表示する場合、毎回SVGの全体を出力するとHTMLが重くなります。そのときは、<symbol> 要素をまとめたスプライトをページに1回だけ出力し、使う場所では <use> で参照します。
<?php
// 子テーマの functions.php に追記
add_action( 'wp_body_open', 'mychild_print_svg_sprite' );
function mychild_print_svg_sprite() {
?>
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
<symbol id="icon-check" viewBox="0 0 24 24">
<path d="M20 6 9 17l-5-5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</symbol>
</svg>
<?php
}
<svg class="icon" aria-hidden="true" focusable="false"><use href="#icon-check"></use></svg>
スプライトの中身はテーマのコードに直接書いた固定の内容なので、外部から値が入り込む余地がありません。wp_body_open フックはWordPress 5.2で追加されたもので、クラシックテーマの場合はテーマの header.php が wp_body_open() を呼んでいる必要があります。古いテーマで出力されないときは、header.php の <body> 直後を確認してください。ブロックテーマでもこのフックは実行されます。
ユーザーがアップロードしたSVGを扱う場合
投稿者がメディアライブラリにSVGをアップロードし、それをテーマがインライン出力する、という設計はもっとも危険度が高くなります。upload_mimes フィルターでSVGを許可するだけのコード例を見かけますが、これは中身を検査せずに受け入れるだけなので、この記事の目的には使えません。
どうしてもアップロードを許可する場合は、アップロード時にSVGを無害化する Safe SVG のような維持されているプラグインを使い、アップロードできる権限も信頼できるユーザーに限ります。そのうえで、テーマ側では出力時にも wp_kses() を通す二重の対策にしておくと、プラグインの設定変更や停止があっても被害を抑えられます。アップロードされたSVGの表示は、インラインにこだわらず img タグで読み込めば、SVG内のスクリプトは実行されません。色を変える必要がない画像なら、こちらのほうが簡単で安全です。
なお、ブロックエディターの「カスタムHTML」ブロックにSVGを貼り付ける方法は、unfiltered_html 権限を持つ管理者などでなければ要素が除去されます。権限によって結果が変わるため、テーマの部品としてはこの記事の関数方式のほうが管理しやすくなります。
読み上げへの配慮
アイコンには「意味を持つもの」と「装飾にすぎないもの」があります。
- 装飾用:隣に文字ラベルがあるアイコンは
aria-hidden="true"を付けて読み上げから外します。上の関数はこの扱いです。 - 意味を持つもの:アイコンだけでボタンやリンクを表す場合は、
screen-reader-textクラスの文字ラベルを添えるか、SVGにrole="img"と<title>を付けて名前を与えます。
focusable="false" は、古いブラウザでSVGがTabキーのフォーカス対象になってしまう問題への対策として付けています。
動作確認の方法
実装したら、次の点を確認します。
- ページのソースでSVGが出力され、
viewBoxが残っているか - 試しにアイコンファイルへ
<script>を1行加え、出力から消えることを確認する(確認後は元に戻す) - CSSで
.icon { width: 1.25em; height: 1.25em; }のようにサイズを指定し、親要素の文字色に合わせて色が変わるか - スクリーンリーダーやブラウザのアクセシビリティツリーで、装飾用アイコンが読み上げ対象から外れているか
wp_kses() の挙動や WP_HTML_Tag_Processor の機能はWordPressの更新で拡張されることがあるため、大きな更新の後は出力結果を一度見直してください。ここでの説明は執筆時点の仕様に基づいています(2026年10月現在)。
まとめ
- インラインSVGはスクリプトなどを含み得るため、出どころを限定し、出力前に
wp_kses()で許可したタグと属性だけを残します。 - 子テーマの
assets/icons/に置いたファイルを、sanitize_key()で名前を制限した関数から読み込む方式が扱いやすい基本形です。 - 許可リストの属性名は小文字で書きます。
viewboxを大文字で書くと属性が消えます。 - 同じアイコンを繰り返すなら
wp_body_openでスプライトを出力し、<use>で参照します。 - ユーザーがアップロードしたSVGは、無害化プラグインと出力時の
wp_kses()を併用するか、imgタグで表示します。