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.test
やpurifier.settings.youtube
はPurifier::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'],
],
これは左から
- 許可する属性の対象である要素名
- 許可する属性名
- 許可する値
を入力する。
上の例だとEnumで指定しているが他にもいろいろ指定方法はある。
詳しくは公式ドキュメントを。
custom_elements
これはカスタム要素を追加するconfig。
初期では以下のようになっている
'custom_elements' => [
['u', 'Inline', 'Inline', 'Common'],
],
これは左から
- 許可する要素名
- インライン要素かブロック要素か(Inline-Blockはできないみたい)
- Inline: インライン要素
- Block: ブロック要素
- false: 上記に適合しない型。(liやtrなど)
- 許可する子要素
- Empty: 何も許可しない。(brやhrなど)
- Inline: 任意の数のInlineとテキスト。(spanなど)
- Flow: 任意の数のBlock,Inline,テキスト。(divなど)
- 一般的な属性を持つか
- 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つの定義をする
- id
公式ドキュメントのこの部分。
configの'id' => 'html5-definitions',
の部分。
つまり、cacheに変更したことを知らせるためのIDってこと? - rev
公式ドキュメントのこの部分。
configの'rev' => 1,
の部分。
IDは時系列を持たないけど、バージョンで指定すれば時系列がわかるよ! ってこと? - debug
configの'debug' => false,
の部分。
for Laravel特有のものだけど、やってることはCache.DefinitionImplをnullにしてキャッシュを残さないようにしているだけ。
つまり、設定を変えたら毎回読み込みなおしてすぐ反映されるようにしている。 - elements
「custom_elements」で定義できたものをここでもできるようにしている。 - 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が入っていても洗浄してくれる。