HTMLPurifier による HTML サニタイズ


a-blog cms は、エントリー本文などユーザーが入力した HTML をテンプレート変数として出力する際、HTMLPurifier を使って XSS につながる要素・属性を取り除きます。このページでは、標準のサニタイズ処理と、設定・Hook による拡張方法を説明します。

テンプレートでの出力とサニタイズ

テンプレート変数は既定で エスケープされ、HTML タグは < などの文字列として表示されます。エントリー本文のように HTML を HTML のまま出力したいときは raw 校正オプションを付けます。サニタイズ(HTMLPurifier)は、この raw 出力に対して危険な要素・属性だけを取り除き、安全な HTML を残す処理です(エスケープのように全体を文字化するのではなく、HTML の構造を保ったまま無害化します)。

標準テンプレートでのサニタイズの効き方は、校正オプションと config で決まります。

書き方

挙動

{vars}

エスケープして出力(既定・HTML はタグごと文字化され安全)

{vars}[raw]

HTML をそのまま出力。strip_dangerous_tag が on のときは自動でサニタイズされる

{vars}[raw|allow_dangerous_tag]

strip_dangerous_tag が on でもサニタイズせずに出力する。信頼できる HTML や Google Tag Manager などの script タグの出力が必要な場合にのみ利用します。

Twig テンプレートでは {{ entry.body|safe_html }} がサニタイズ付き出力、{{ entry.body|raw }} が非サニタイズ出力にあたります。

サニタイズが適用されるタイミング

サニタイズは HTML を保存するときではなく、出力するときに適用されます。データベースに保存されている HTML 自体が書き換わるわけではありません。代表的には次の場面です。

  • エントリー本文などのHTML文字列を raw でテンプレート出力するとき

  • ページキャッシュを再生成するとき

  • システム更新時にコンテンツを再生成するとき

出力のたびに適用されるため、dangerous_tags や iframe の許可ホストといった設定を変更すると、既存のエントリーもキャッシュの再生成タイミングで新しい方針に沿って出力し直されます。

自作のモジュールや拡張から同じサニタイズ処理を利用したい場合は、次のように呼び出せます。

$html = Acms\Services\Facades\Application::make('html-purifier')->clean($html);

サニタイズ設定

有効無効 (strip_dangerous_tag)

標準テンプレートでraw 校正オプション利用時の変数の出力時にデフォルトで HTMLPurifier を利用したサニタイズを適用するかどうかを設定します。

strip_dangerous_tag: on # on | off 変数から標準で危険なタグ(dangerous_tagsで指定)を削除するか指定

新規インストール時のデフォルトは on です。

禁止タグ(dangerous_tags)

private/config.system.default.yaml のデフォルト値は次のとおりです。

dangerous_tags: ['form']

ここに列挙したタグは丸ごと削除されます。<font> など HTML 標準で非推奨・廃止されたタグは、dangerous_tags の指定に関わらず常に削除されます。

iframe は dangerous_tags ではなく、次に説明する whitelist で制御します。

iframe のホスト whitelist (Ver. 3.2.25 ~)

html_purifier_iframe_allowed_hosts で、src を許可するホスト(パス prefix 可)を指定します。

html_purifier_iframe_allowed_hosts: [
    'www.youtube.com/embed|fullscreen; encrypted-media; picture-in-picture|',
    'www.youtube-nocookie.com/embed|fullscreen; encrypted-media; picture-in-picture|',
    'player.vimeo.com/video|fullscreen; picture-in-picture|',
    'embed.nicovideo.jp/watch/|fullscreen; encrypted-media; picture-in-picture|',
    'www.dailymotion.com/embed/video|fullscreen; picture-in-picture|',
    'player.twitch.tv|fullscreen|',
    'embed.ted.com/talks|fullscreen|',
    'streamable.com/e/|fullscreen|',
    'players.brightcove.net|fullscreen; encrypted-media|',
    'fast.wistia.net/embed/iframe|fullscreen|',
    'www.google.com/maps/embed',
    'docs.google.com/forms|',
    'calendar.google.com/calendar/embed|',
    'lookerstudio.google.com/embed/reporting|fullscreen|',
    'docs.google.com/document/d/e/|fullscreen|',
    'docs.google.com/spreadsheets/d/e/|',
    'docs.google.com/presentation/d/e/|fullscreen|',
    'docs.google.com/drawings/d/e/|',
    'open.spotify.com/embed/|encrypted-media|',
    'voicy.jp/embed/|',
    'stand.fm/embed/channels/|',
    'stand.fm/embed/episodes/|',
    'w.soundcloud.com/player|',
    'embed.podcasts.apple.com|encrypted-media|',
    'speakerdeck.com/player/|fullscreen|',
    'www.slideshare.net/slideshow/embed_code/|fullscreen|',
    'note.com/embed/notes/|',
    'hatenablog-parts.com/embed|',
    'e.issuu.com/embed.html|fullscreen|',
    'www.scribd.com/embeds|',
    'www.figma.com/embed|fullscreen|',
    'www.canva.com/design/|fullscreen|',
    'codepen.io|fullscreen|',
    'codesandbox.io/embed|fullscreen|',
    'miro.com/app/live-embed|fullscreen|',
    'sketchfab.com/models|fullscreen|',
    'map.yahoo.co.jp/embed|',
    'www.navitime.co.jp/maps/embed|',
]

書式は host[/pathPrefix][|allow[|sandbox]] です。

要素

説明

host[/pathPrefix]

許可する src の前方一致。境界は / ? # または文字列終端(例: www.youtube.com/embed は /embed/... のみ許可し /watch は拒否)

allow

セミコロン区切りの Permissions-Policy 機能トークン(例: fullscreen; encrypted-media)

sandbox

スペース区切りの sandbox トークン

allow / sandbox は「編集者が要求できる capability の許可リスト」です。エントリー側の <iframe> に書かれた allow(セミコロン区切り)/ sandbox(スペース区切り)のうち、ここに列挙したトークンだけが残り、許可リスト外のトークンは削除されます。編集者が allow / sandbox を書いていなければ何も付与されません(強制で付与されることはありません)。ホスト部分だけを書いた場合は許可リストが空になり、編集者が何を書いても capability が付与されない最小権限になります。

  • 許可ホストにマッチしない <iframe> は、src だけが取り除かれて空の <iframe> として残ります(要素ごと削除されるわけではありません)。

  • html_purifier_iframe_allowed_hosts: [] にすると、すべての iframe の src が無効化されます。

  • 設定は private/config.system.yaml 側で上書き(追加・削除)できます。

iframe の許可ホストを追加する

html_purifier_iframe_allowed_hosts を private/config.system.yaml に直接設定すると、コアの既定リストを置き換えます。既定の許可ホストを保ったまま追加する場合は、extends_html_purifier_iframe_allowed_hosts を使います。

extends_html_purifier_iframe_allowed_hosts:
  - example.com/embed|

指定形式は html_purifier_iframe_allowed_hosts と同じです。設定値を変更すると、HTMLPurifier の定義キャッシュは自動で再生成されます。

script のホスト許可リスト

外部サービスの埋め込みなどで必要になる外部 script は、ホストの許可リストで制御します。既定の禁止タグは次のとおりです。

dangerous_tags: ['form']

script タグは一律に禁止せず、html_purifier_script_allowed_hosts に登録したホストのものだけを許可します。インライン script は許可されません。この設定は Ver. 3.2.36 以降で利用できます。

許可ホストを追加する

既定の許可ホストを残したまま追加する場合は、extends_html_purifier_script_allowed_hosts を使います。

extends_html_purifier_script_allowed_hosts:
  - example.com

html_purifier_script_allowed_hosts を直接設定すると既定の許可リストを置き換えます。既定の埋め込みサービスも継続して利用する場合は、通常は extends_... を使用してください。


HtmlPurifier によるサニタイズは HTML の構造・属性の許可リストを前提に構成されています。iframe whitelist は src を縛るものであり、フレーム表示の最終的な可否はHTTPヘッダー側の X-Frame-Options / CSP frame-ancestors にも依存します。多層防御の観点では、CSP の frame-src も別途あわせて運用してください。


Hook による拡張

dangerous_tags や whitelist だけで表現できない要件(独自タグ・属性の許可、URI スキームの追加など)は、extendsHtmlPurifierConfig / extendsHtmlPurifierDefinition の 2 つのフックで拡張します。サイト拡張(extension/acms/Hook.php)やプラグインのフッククラスに実装します。

2 つのフックの違い(Ver. 3.2.25 ~)

フック

発火タイミング

用途

extendsHtmlPurifierConfig(HTMLPurifier_Config $config)

カスタム HTML 定義を取得する前

HTML.* / URI.* / CSS.* / Attr.* の config を上書き・追加する

extendsHtmlPurifierDefinition(HTMLPurifier_HTMLDefinition $def, HTMLPurifier_Config $config)

カスタム HTML 定義を取得した後

addElement / addAttribute で許可する要素・属性を追加する

HTML.* 系の設定は定義の取得前に確定させる必要があるため、許可タグ・属性・URI スキームの調整は必ず extendsHtmlPurifierConfig 側で行ってください。定義取得後にしか行えない要素・属性の追加(addElement / addAttribute)は extendsHtmlPurifierDefinition 側で行います。

/**
 * HTMLPurifier の設定構築時に呼ばれる拡張ポイント(カスタム HTML 定義の取得前)
 */
public function extendsHtmlPurifierConfig(\HTMLPurifier_Config $config): void
{
    // 例: data: スキームの画像を許可する
    $config->set('URI.AllowedSchemes', ['http' => true, 'https' => true, 'mailto' => true, 'data' => true]);

    // 例: 特定タグをさらに禁止する(既定値を上書き)
    $config->set('HTML.ForbiddenElements', ['script', 'iframe', 'form', 'object']);
}

/**
 * HTMLPurifier のカスタム HTML 定義の構築時に呼ばれる拡張ポイント(定義の取得後)
 */
public function extendsHtmlPurifierDefinition(\HTMLPurifier_HTMLDefinition $def, \HTMLPurifier_Config $config): void
{
    // 例: <details> / <summary> を許可し、独自 data 属性を通す
    $def->addElement('details', 'Block', 'Flow', 'Common', ['open' => 'Bool#open']);
    $def->addElement('summary', 'Inline', 'Inline', 'Common');
    $def->addAttribute('div', 'data-my-widget', 'CDATA');
}

雛形(ablogcms/extension/acms/Hook.php)にも同じ形の空実装とサンプルコメントが用意されています。


キャッシュ

カスタム HTML 定義はキャッシュされます。iframe whitelist(html_purifier_iframe_allowed_hosts)を config で変更した場合は自動でキャッシュが再生成されますが、extendsHtmlPurifierConfig / extendsHtmlPurifierDefinition フックで追加・変更した内容は自動では反映されません。

フックの実装を追加・変更したときは、必ず管理画面からキャッシュを削除してください。 削除すると html-purifier 用のキャッシュディレクトリが消え、次回アクセス時に新しい定義でキャッシュが再生成されます。挙動が反映されていないように見えたときも、まずキャッシュ削除を試してください。


注意点

  • extendsHtmlPurifierConfig / extendsHtmlPurifierDefinition はフック機能が有効(HOOK_ENABLE)であることが前提です。フックを無効化している環境では呼ばれません。

  • 独自属性を許可する際は、フリーフォームの CDATA を安易に増やさず、可能な限り Enum や Bool など型付きの AttrDef を使ってください。