Laravel の Blade で {{ と }} に囲まれたものは全てエスケープされるわけじゃない

こないだ Laravel の話をしていて話題になったのでメモしておきます。

Blade について

Laravel にはデフォルトで Blade というテンプレートエンジンが組み込まれてますね。

初めてさわったときはちょっととっつきにくい感じがしたんだけど、しばらく使ってみるとなかなか便利に思えてきた。

前提

さて Blade テンプレートの中で何らかの式を {{}} で囲むとその値が出力されるんだけど

Hello, {{ $name }}.

この処理の過程でその値は htmlentities 関数を通過します。基本的には。

Blade {{ }} statements are automatically sent through PHP’s htmlentities function to prevent XSS attacks.

上記の例で $name<script>alert("こんにちは")</script> などというものが入っていても &lt;script&gt;alert(&quot;こんにちは&quot;)&lt;/script&gt; に変換されるから、スクリプトは発動しない。

デフォルトで勝手にやってくれるから、ユーザー由来の値を出力するたびにエスケープ処理をする必要がなくて楽ちん。

逆に、意図的に HTML タグを出力したい場合などは {!!!!} を使うことでそのまま出力されるようになってます。

{{ csrf_field() }} への疑問

ところが、Laravel の公式ドキュメントにこんなコードが書かれています。

// Blade Template Syntax
{{ csrf_field() }}

これを書くと次のコードが出力されるとのこと。

<input type="hidden" name="_token" value="<?php echo csrf_token(); ?>">

これはどうしたことか。

{{}} で囲まれた csrf_field() の値は htmlentities を通過するから HTML 要素としては機能しないはず。

<?php echo csrf_field(); ?> とか <?= csrf_field() ?> とか {!! csrf_field() !!} にしないとダメなはず。

それなのに、実際にこれを書くと HTML として正しく出力されます。

例外がある

要するに上で書いた {{ }} の処理の過程には例外があります。実際にはエスケープを行う e() ヘルパーに Laravel 5.1 から例外的な処理が追加されました。

e() の処理

Laravel 5.0 まではこうでした。

function e($value)
{
    return htmlentities($value, ENT_QUOTES, 'UTF-8', false);
}

単純に htmlentities を通してるだけ。

これが、Laravel 5.1 からはこう。

function e($value)
{
    if ($value instanceof Htmlable) {
        return $value->toHtml();
    }
    return htmlentities($value, ENT_QUOTES, 'UTF-8', false);
}

引数の $valueHtmlable を実装したクラスのインスタンスなら $value->toHtml() をそのまま返す。

ということで {{ }} に囲まれた式の値が Htmlable を実装していたら、処理が変わってきます。

csrf_field() の処理

では先ほどの csrf_field() はどうなっているかというと

function csrf_field()
{
    return new HtmlString('<input type="hidden" name="_token" value="'.csrf_token().'">');
}

ここで返されている HtmlString クラスがこれ。

//コメントは省略
class HtmlString implements Htmlable
{
    protected $html;

    public function __construct($html)
    {
        $this->html = $html;
    }

    public function toHtml()
    {
        return $this->html;
    }

    public function __toString()
    {
        return $this->toHtml();
    }
}

Htmlable インターフェイスを実装しています。コンストラクタの引数として与えられた HTML タグは html プロパティに保存され、toHtml() によって返されます。

結論

ということで Blade テンプレートに {{ csrf_field() }} と書いた場合、 HTML タグが htmlentities にかけられることなくそのまま出ます。

なお <?php echo csrf_field(); ?> と書いて直接出力した場合も、HtmlString__toString() によってやはり toHtml() が呼ばれるから同じ結果になりますね。

要するにどっちでもいい。

他の例

Eloquent モデルの paginate() で返されたコレクションは $posts->render() などとすると自動でページネーションのタグを吐いてくれるんだけど、これも以前は HTML 文字列をそのまま返していたのが、いまは

public function render()
{
    if ($this->hasPages()) {
        return new HtmlString(sprintf(
            '<ul class="pagination">%s %s %s</ul>',
            $this->getPreviousButton(),
            $this->getLinks(),
            $this->getNextButton()
        ));
    }
    return '';
}

となってます。

HtmlString クラスのインスタンスを返してるから {{ $posts->render() }} としても正しく HTML が出る。ただしこちらは Laravel 5.2 からです。

取り込んで使える

この仕組みはフレームワークの組み込みヘルパーだけじゃなくて、アプリケーションへ取り込んで使うことができますね。

HTML を吐き出すためのクラスなりそれを返す関数なりを作って Htmlable を実装しておけば {{ }} が使える。

まあそんなことしなくても普通に echo したり {!! !!} を使ったりすればいいんだけど、{{ }} が「ビュー側ではエスケープ処理のことを心配しなくてもいいようにしよう」という設計になっているのだとすると、それに倣っていっそもう全部 {{ }} で出力することにしてもいいんじゃないかな。基本的にはエスケープありで、HTML をそのまま吐きたいときだけ事前に HTML として出力可能なものとして用意しておくというのはアリな気がします。まさに HTMLable.

それにしてもこれ何て読むんだろう。エイチティーエムエラボー?

関連エントリ

  • このエントリーをはてなブックマークに追加