HTMLPurifier for Laravelを手探りで解説する

投稿日 最終更新日

2024年10月23日

※過去ブログから一部切り出したものです

このHTMLPurifier for LaravelってGitHubにWikiもないし使い方の解説が無い。
なんなら日本語の解説ブログも無い。
その為、かなり手探りで理解を進めていた。

最近大まかに理解できたので一回ここにまとめる。

前提

前提として

  • HTMLPurifierはHTMLを洗浄(Purifier)し悪意のあるコードやHTMLの文法チェックをしてくれるもの
  • HTMLPurifier for LaravelはHTMLPurifierをLaravelで手軽に使えるようにしたものなので、本家はHTMLPurifierである
  • その為、ドキュメントはHTMLPurifierのドキュメントを見ると良い
  • 手探りなので、間違っていたりミスリードしている可能性がある

インストール

HTMLPurifier for LaravelのGitHubに書いてある。

Laravel5.5以降の場合は
composer require mews/purifier
するだけ。 Laravel10や11にも対応しているので安心うれしい。

使い方

これもGitHubに書いてあるけど一応。

まず、以下を使いたいところに入れる。

use Mews\Purifier\Facades\Purifier;

その上で以下のようにすると、デフォルトの設定で洗浄してくれる。

Purifier::clean("洗浄したいHTML");

以下のようにすると、指定した設定を適用できる。
この場合はtitlesという設定で洗浄している。

Purifier::clean('洗浄したいHTML', 'titles');

以下のようにすると設定を上書きできる。

Purifier::clean('This is my H1 title', array('Attr.EnableID' => true));

HTMLPurifier for Laravelのconfig

以下のコマンドでPurifierのconfigを公開し、config/purifier.phpで弄れるようにする。

php artisan vendor:publish --provider="Mews\Purifier\PurifierServiceProvider"

以下が初期のconfigファイル

<?php
/**
* Ok, glad you are here
* first we get a config instance, and set the settings
* $config = HTMLPurifier_Config::createDefault();
* $config->set('Core.Encoding', $this->config->get('purifier.encoding'));
* $config->set('Cache.SerializerPath', $this->config->get('purifier.cachePath'));
* if ( ! $this->config->get('purifier.finalize')) {
*     $config->autoFinalize = false;
* }
* $config->loadArray($this->getConfig());
*
* You must NOT delete the default settings
* anything in settings should be compacted with params that needed to instance HTMLPurifier_Config.
*
* @link http://htmlpurifier.org/live/configdoc/plain.html
*/


return [
   'encoding'           => 'UTF-8',
   'finalize'           => true,
   'ignoreNonStrings'   => false,
   'cachePath'          => storage_path('app/purifier'),
   'cacheFileMode'      => 0755,
   'settings'      => [
       'default' => [
           'HTML.Doctype'             => 'HTML 4.01 Transitional',
           'HTML.Allowed'             => 'div,b,strong,i,em,u,a[href|title],ul,ol,li,p[style],br,span[style],img[width|height|alt|src]',
           'CSS.AllowedProperties'    => 'font,font-size,font-weight,font-style,font-family,text-decoration,padding-left,color,background-color,text-align',
           'AutoFormat.AutoParagraph' => true,
           'AutoFormat.RemoveEmpty'   => true,
       ],
       'test'    => [
           'Attr.EnableID' => 'true',
       ],
       "youtube" => [
           "HTML.SafeIframe"      => 'true',
           "URI.SafeIframeRegexp" => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/)%",
       ],
       'custom_definition' => [
           'id'  => 'html5-definitions',
           'rev' => 1,
           'debug' => false,
           'elements' => [
               // http://developers.whatwg.org/sections.html
               ['section', 'Block', 'Flow', 'Common'],
               ['nav',     'Block', 'Flow', 'Common'],
               ['article', 'Block', 'Flow', 'Common'],
               ['aside',   'Block', 'Flow', 'Common'],
               ['header',  'Block', 'Flow', 'Common'],
               ['footer',  'Block', 'Flow', 'Common'],
           
            // Content model actually excludes several tags, not modelled here
               ['address', 'Block', 'Flow', 'Common'],
               ['hgroup', 'Block', 'Required: h1 | h2 | h3 | h4 | h5 | h6', 'Common'],
           
            // http://developers.whatwg.org/grouping-content.html
               ['figure', 'Block', 'Optional: (figcaption, Flow) | (Flow, figcaption) | Flow', 'Common'],
               ['figcaption', 'Inline', 'Flow', 'Common'],
           
            // http://developers.whatwg.org/the-video-element.html#the-video-element
               ['video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
                   'src' => 'URI',
               'type' => 'Text',
               'width' => 'Length',
               'height' => 'Length',
               'poster' => 'URI',
               'preload' => 'Enum#auto,metadata,none',
               'controls' => 'Bool',
               ]],
               ['source', 'Block', 'Flow', 'Common', [
               'src' => 'URI',
               'type' => 'Text',
               ]],


            // http://developers.whatwg.org/text-level-semantics.html
               ['s',    'Inline', 'Inline', 'Common'],
               ['var',  'Inline', 'Inline', 'Common'],
               ['sub',  'Inline', 'Inline', 'Common'],
               ['sup',  'Inline', 'Inline', 'Common'],
               ['mark', 'Inline', 'Inline', 'Common'],
               ['wbr',  'Inline', 'Empty', 'Core'],
           
            // http://developers.whatwg.org/edits.html
               ['ins', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
               ['del', 'Block', 'Flow', 'Common', ['cite' => 'URI', 'datetime' => 'CDATA']],
           ],
           'attributes' => [
               ['iframe', 'allowfullscreen', 'Bool'],
               ['table', 'height', 'Text'],
               ['td', 'border', 'Text'],
               ['th', 'border', 'Text'],
               ['tr', 'width', 'Text'],
               ['tr', 'height', 'Text'],
               ['tr', 'border', 'Text'],
           ],
       ],
       'custom_attributes' => [
           ['a', 'target', 'Enum#_blank,_self,_target,_top'],
       ],
       'custom_elements' => [
           ['u', 'Inline', 'Inline', 'Common'],
       ],
   ],
];

ここで主な設定を弄れるが、一番上のコメントにある通り

You must NOT delete the default settings.
anything in settings should be compacted with params that needed to instance HTMLPurifier_Config.

デフォルトの設定は削除しちゃだめ。

デフォルトっていうのは
'default' => [
で定義されている設定のことだろう。

configを簡単に説明する

めっちゃ簡単に説明すると、

  • HTML.Allowedに許可したいタグ名と属性を入れればおk!
  • CSS.AllowedPropertiesに許可したいCSS名入れればおk!
  • ホワイトリスト形式だから、Allowedに入れないと許可されないよ!

以上。

正直、今挙げたものだけを弄れば大抵のことは済むんだけど、audioタグ等の新しめなタグを使いたい場合はちゃんと理解しないといけない。

configをちゃんと説明する

configの配列で定義されているのは主に以下の6つ。

encoding

本家ドキュメントのこの部分が該当。
configでいう'encoding' => 'UTF-8',の部分。

とにかく、UTF-8でエンコードする分には問題なさそう。

finalize

本家ドキュメントのこの部分が該当。finalizeの説明についてはここが該当。
configの'finalize' => true,の部分。

これがtrueだと設定を後から変えられないようにできる。
falseだと設定を後から変えられる。

ignoreNonStrings

本家ドキュメントにない、for Laravel特有の機能。
configの'ignoreNonStrings' => false,の部分

コードを探したら以下の部分が該当。
vendor\mews\purifier\src\Purifier.php

//If $dirty is not an explicit string, bypass purification assuming configuration allows this
$ignoreNonStrings = $this->config->get('purifier.ignoreNonStrings', false);
$stringTest = is_string($dirty);
if($stringTest === false && $ignoreNonStrings === true) {
   return $dirty;
}


return $this->purifier->purify($dirty, $configObject);

つまりtrueにしていると、取得したHTMLがString型じゃない場合purifierせずにreturnするようになる。
falseにしていると、一応purifierしてくれるっぽい。

cachePath

本家ドキュメントにない、for Laravel特有の機能。
configの'cachePath' => storage_path('app/purifier'),の部分。

configのキャッシュを保存する場所の指定。

cacheFileMode

configの'cacheFileMode' => 0755,の部分。

キャッシュを保存するディレクトリのパーミッション。
ちなみに、上2つは以下のコードで利用されている。
vendor\mews\purifier\src\Purifier.php

private function checkCacheDirectory()
{
   $cachePath = $this->config->get('purifier.cachePath');


   if ($cachePath) {
       if (!$this->files->isDirectory($cachePath)) {
           $this->files->makeDirectory($cachePath, $this->config->get('purifier.cacheFileMode', 0755),true);
       }
   }
}

settings

一番大事でよく弄る部分。

ここで許可したいHTMLタグや定義されていないタグを定義する。
一番大事なので下で詳しく解説する。

configのsettingsを詳しく理解する

前述したとおり一番大事な部分だし、軽く使うならここのdefaultに定義されているHTML.Allowedとかを弄れば良い。
しかし、ちょっと深いところまで弄りたいならここをしっかり理解したほうが良い。

settings関連のコードはvendor\mews\purifier\src\Purifier.phpのgetConfigメソッドにある。

該当部分を切り抜くと以下のようになる。
私の理解でコメントも付けておいた。

// configの新しいオブジェクトを作成
$configObject = HTMLPurifier_Config::createDefault();


// configの変更を許可するか否か。config/purifier.phpで定義されているものを利用している。
if (! $this->config->get('purifier.finalize')) {
   $configObject->autoFinalize = false;
}


// configの初期設定。config/purifier.phpで定義されているものを利用している。
$defaultConfig = [];
$defaultConfig['Core.Encoding'] = $this->config->get('purifier.encoding');
$defaultConfig['Cache.SerializerPath'] = $this->config->get('purifier.cachePath');
$defaultConfig['Cache.SerializerPermissions'] = $this->config->get('purifier.cacheFileMode', 0755);


// もし使うconfigが指定されていたらそれを。無指定ならdefaultを使う。
if (! $config) {
   $config = $this->config->get('purifier.settings.default');
} elseif (is_string($config)) {
   $config = $this->config->get('purifier.settings.' . $config);
}


// 指定されたconfigが配列じゃないなら配列に
if (! is_array($config)) {
   $config = [];
}


// configを初期設定したものとmerge。
$config = $defaultConfig + $config;


// 出来上がったconfigを$configObjectにロード
$configObject->loadArray($config);


// custom_definitionが定義されていれば、カスタムタグを追加
if ($definitionConfig = $this->config->get('purifier.settings.custom_definition')) {
   $this->addCustomDefinition($definitionConfig, $configObject);
}


// custom_elementsが定義されていれば、カスタム要素を追加
if ($elements = $this->config->get('purifier.settings.custom_elements')) {
   if ($def = $configObject->maybeGetRawHTMLDefinition()) {
       $this->addCustomElements($elements, $def);
   }
}


// custom_attributesが定義されていれば、カスタム属性を追加
if ($attributes = $this->config->get('purifier.settings.custom_attributes')) {
   if ($def = $configObject->maybeGetRawHTMLDefinition()) {
       $this->addCustomAttributes($attributes, $def);
   }
}


return $configObject;

つまりこのことからわかることは、変に指定しない限りpurifier.settings.defaultに定義されたものが使われるということ。

なので、初期から設定されているpurifier.settings.testpurifier.settings.youtubePurifier::clean("洗浄したいHTML","test")のように指定しない限り何も関与しない。

また、

  • custom_definition
  • custom_elements
  • custom_attributes

という特別なconfigがあることもわかった。

custom_definition、custom_elements、custom_attributesとは何か

前提として、HTMLPurifierは事前に定義されていないタグ名or属性名のものがあった場合、エラーを吐くようになっている

そして、HTMLPurifierはXHTML1.1想定してタグ名or属性名をデフォルトで定義している

逆を言えば、HTML5.0で登場したタグoEmbedなどのオリジナルタグはHTMLPurifierに定義されていない

その為、定義されていないタグや属性を使いたい場合、この「custom_definition、custom_elements、custom_attributes」のconfigに自分で定義しなきゃいけないということ。

さっきのconfigのcustom_difinitionをちょこっと見てみると

['section', 'Block', 'Flow', 'Common'],
['nav',     'Block', 'Flow', 'Common'],
['article', 'Block', 'Flow', 'Common'],
['aside',   'Block', 'Flow', 'Common'],
['header',  'Block', 'Flow', 'Common'],
['footer',  'Block', 'Flow', 'Common'],

のようになんか既に定義されている。

これはfor Laravelを作った作者さんの優しさで頻繁に使いそうなHTML5.0のタグを事前定義してくれたってことだと思う。
だからXHTML1.1にないarticleタグとかをHTML.Allowedに入れても未定義エラーにならないんだね。

タグの定義方法

これはHTMLPurifierの公式ドキュメントにそれに近いものの記載があるのでそれを見るのが良いと思う。

やることは、custom_definition、custom_elements、custom_attributesそれぞれに適切な定義をしてあげる感じ。

一応自分の理解の為にも以下に記す。
理解のしやすさのためにcustom_difinitionを最後にする。

custom_attributes

これはカスタム属性を追加するconfig。
初期では以下のようになっている

'custom_attributes' => [
   ['a', 'target', 'Enum#_blank,_self,_target,_top'],
],

これは左から

  1. 許可する属性の対象である要素名
  2. 許可する属性名
  3. 許可する値

を入力する。

上の例だとEnumで指定しているが他にもいろいろ指定方法はある。
詳しくは公式ドキュメントを。

custom_elements

これはカスタム要素を追加するconfig。
初期では以下のようになっている

'custom_elements' => [
   ['u', 'Inline', 'Inline', 'Common'],
],

これは左から

  1. 許可する要素名
  2. インライン要素かブロック要素か(Inline-Blockはできないみたい)
    • Inline: インライン要素
    • Block: ブロック要素
    • false: 上記に適合しない型。(liやtrなど)
  3. 許可する子要素
    • Empty: 何も許可しない。(brやhrなど)
    • Inline: 任意の数のInlineとテキスト。(spanなど)
    • Flow: 任意の数のBlock,Inline,テキスト。(divなど)
  4. 一般的な属性を持つか
    • I18N: lang属性を持つ
    • Core: style, class, id, titleの属性を持つ
    • Common: 上2つどちらも

こんな感じ。許可する子要素は説明したもの以外にも細かく指定できる。
詳しくは公式ドキュメントを。

また、上の例には無いが5つ目の引数で属性を指定できる。
以下の例がわかりやすい。

['video', 'Block', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', [
    'src' => 'URI',
    'type' => 'Text',
    'width' => 'Length',
    'height' => 'Length',
    'poster' => 'URI',
    'preload' => 'Enum#auto,metadata,none',
    'controls' => 'Bool',
]],

2次配列で指定できるんだね。

custom_definition

こいつがメインの定義config。

主に以下の5つの定義をする

  1. id
    公式ドキュメントのこの部分
    configの'id' => 'html5-definitions',の部分。
    つまり、cacheに変更したことを知らせるためのIDってこと?
  2. rev
    公式ドキュメントのこの部分
    configの'rev' => 1,の部分。
    IDは時系列を持たないけど、バージョンで指定すれば時系列がわかるよ! ってこと?
  3. debug
    configの'debug' => false,の部分。
    for Laravel特有のものだけど、やってることはCache.DefinitionImplをnullにしてキャッシュを残さないようにしているだけ。
    つまり、設定を変えたら毎回読み込みなおしてすぐ反映されるようにしている。
  4. elements
    「custom_elements」で定義できたものをここでもできるようにしている。
  5. attributes
    「custom_attributes」で定義できたものをここでもできるようにしている。

わざわざcustom_definitionで要素と属性を定義する意味はなんだろうと思うよね。
ソースコード見た感じだと、IDとVerの指定ができるってのと、Debugが指定できるのがいいねって感じだった。

for Laravelを作った人的にcustom_definitionを使ってくれ感がすごいので、私はcustom_definitionで定義していこうと思う。

URIのスキームを指定する

安全なウェブサイトの作り方のXSSにもある通り、URLを出力させるときはスキームを「http」や「https」のみ許可したほうが良い。

HTMLPurifierでは属性の持てる値でURIというのを指定すると、「javascript:」などのURLを入れても洗浄してくれるようになる。

じゃあURIを指定するとデフォルトで何が許可されているのかってところなんだけど、公式ドキュメントによると以下の7つが許可されているみたい。

array (
 'http' => true,
 'https' => true,
 'mailto' => true,
 'ftp' => true,
 'nntp' => true,
 'news' => true,
 'tel' => true,
)

httpとhttpsはわかるんだけど、それ以外はそもそも知らないし必要ないと思うので許可したくない。

そんな場合はdefaultとかに

'URI.AllowedSchemes' => 'http, https',

を指定してあげればおk。 こうすればmailto:example@example.com?subject=件名みたいなURLが入っていても洗浄してくれる。