ブラウザに文字列を出力する上で、エスケープ処理(サニタイジング)をすることはとても重要です。
もし、この対策をしてなかった場合、脆弱性が存在することになり、クロスサイトスクリプティング(XSS)に対して無防備な状態になるので欠かすことができません。
Drupal 8 のTwigはデフォルトの設定で、変数は自動的にエスケープ処理がされます。Drupal 7までは常にcheck_plain()を使うことを気にする必要がありましたが、もう意識する必要はありません。
#検証環境
- Macbook Pro (Retina, 15-inch, Mid 2014)
- Mac OS X 10.11.6 El Capitan
- PHPStorm 2017.3
- Vagrant 1.9.4
- CentOS 7.4.1708
- Drupal8.4.4
#Twigは自動的にエスケープ処理がされる
さっそく、HTMLタグなどの特殊文字を出力したときに、どうなるのか見てみましょう。
例)特殊文字を表示してみる
{% set unsafe_html = '<a href="javascript:alert(\'Test\')">Test</a>' %}
{{ unsafe_html }}
出力内容をブラウザのソース画面から確認すると、HTMLタグやダブルクオートがエスケープ処理されたことが確認できます。
<a href="javascript:alert('Test')">Test</a>
あるいは、エスケープ処理することを明示的にする場合は、eフィルター(escapeフィルターのエイリアス)を利用します。
{{ unsafe_html|e }}
反対に、あえてエスケープ処理をスキップしたい場合、rawフィルターを利用します。
{{ unsafe_html|raw }}
rawフィルターを利用した場合、入力データがそのまま出力されるのでJavascriptコードが実行されることになるのでご注意ください。
###HTML以外の言語をエスケープする
eフィルターはデフォルトだとHTMLエスケープが適用されますが、HTML以外に対応する場合は引数にいずれかのコンテキスト(※)を指定します。
※コンテキストの種類は、html, js, css, url, html_attr です
1. JavaScriptコード内の変数をエスケープする
<script>
var username = "{{ user.username|e('js') }}";
</script>
2. CSSコードの文字列をエスケープする
<div style="{{ inline_css|e('css') }}">・・・</div>
マルチバイト文字や絵文字など、英数字以外のすべての文字列をエスケープします。
3. URLに含まれるるディレクトリパスや、パラメーターをエスケープする
<a href="https://my-web.com?param01{{ param01|e('url') }}" rel="bookmark">Text link</a>
動的に埋めこまれるディレクトリパスや、パラメーターの文字列をエスケープします。(具体的にはURLエンコード処理がされます)
URL全体 https://〜
を格納してしまうとコロンやスラッシュの部分がエスケープされてしまい、URLとして機能しなくなるので、基本的にはパスなどの一部の文字列を対象にしましょう。
もし、URL全体が格納された変数を安全に埋めこむには、後述するtフィルターが最適です。
4. HTML属性で利用できない文字列をエスケープする
<img src="/img/logo.png" alt="{{ label|e('html_attr') }}">{{ label }}</a>
#tフィルターを利用してURLを安全に埋めこむ
href属性にURLを埋めこむ場合は、このように書くことがあると思います。
<a href="{{ item.url }}">{{ item.text }}</a>
ただ、この場合は変数内に悪意のあるJavascriptコードが埋め込まれてしまう可能性があります。
対処方法としてvar|e('url')
を使ってURLをエスケープした場合、https://
のコロンとスラッシュの部分までもがエスケープされてしまうため、URLとして判断できない文字列になってしまいます。
一方、tフィルターのプレースホルダー「:」を利用すると、本当に危険なコードだけがエスケープされるため、URLが格納された変数を安心して埋めこむことができます。
例)tフィルターによって安全にURLを埋めこむ
{% set url = 'http://google.com/&"<>' %}
{{ '<p class="btn"><a href=":url">link</a></p>'|t({ ':url': url }) }}
###それぞれのフィルターでURLをエスケープしてみる
tフィルターとeフィルターの出力結果の違いを見てみましょう。
実行コード
{% set unsafe_url = "javascript:yahoo.co.jp?aaa=aaaaa&bbb=bbb<>!" %}
{#tフィルターを使って安全な文字列に変換する#}
{{ ':url'|t({ ':url': unsafe_url }) }}
{#eフィルターを使ってURLをエスケープする#}
{{ unsafe_url|e('url') }}
{#URLエンコードする#}
{{ unsafe_url|url_encode }}
結果コード
yahoo.co.jp?aaa=aaaaa&bbb=bbb<>!
javascript%3Ayahoo.co.jp%3Faaa%3Daaaaa%26bbb%3Dbbb%3C%3E%21
javascript%3Ayahoo.co.jp%3Faaa%3Daaaaa%26bbb%3Dbbb%3C%3E%21
このようにtフィルターはjavascript:
などの不正なプロトコルが埋めこまれている場合などに除去されますが、URLエンコードまではやってくれません。
必要に応じて、eフィルター、またはurl_encodeフィルターと併用してください。
#HTML属性や特定の書式をエスケープしてくれるフィルター
表示用テキスト以外の対応として、idやclass専用のフィルターなどが用意されています。
###clean_class
有効なHTMLクラスとして文字列をフィルターします。
具体的には、不正なタグが除去されるほか、スペースやアンダースコアがハイフンに置換されたり、小文字に変換されます。
BEMの命名規則を採用している場合はアンダースコアが置換されないよう、使いどころを気を付ける必要はあります。
<div {{ region_attributes[region].addClass('layout__region--' ~ region|clean_class) }}>
内部的にはgetClass
メソッドを呼び出します。
\Drupal\Component\Utility\Html::getClass()
###clean_id
安全なHTMLのidとして文字列をフィルターします。機能はclean_class
と同等です。
<div id="{{ attributes.id|clean_id }}">
内部的にはgetId
メソッドを呼び出します。
\Drupal\Component\Utility\Html::getId()
###format_date
書式設定された日付文字列を準備します。
引数に指定されたUNIXタイプスタンプを元に、'long'などの日付フォーマットに整形して出力します。
{% set created = node.created.value|format_date('long') %}
{% set changed = node.changed.value|format_date('long') %}
内部的にはformat
メソッドを呼び出します。
\Drupal\Core\Datetime\DateFormatter::format
###render
レンダリング配列を元に、HTMLコードを安全な状態で出力します。
{{ content.body|render|length }}
ただ、{{・・・}}
で囲われた変数はrenderフィルターを使わなくてもレンダリングされるため、基本的には上のコードのようにほかのフィルターと併用するときに利用します。
内部的にはrenderVar
メソッドを呼び出します。
\Drupal\Core\Template\TwigExtension::renderVar
###safe_join
区切り文字により文字列を結合した上で、安全な内容で出力します。
{{ tags|safe_join(", ") }}
内部的にはsafeJoin
メソッドを呼び出します。
\Drupal\Core\Template\TwigExtension::safeJoin
#参考ページ
*TwigExtension | TwigExtension.php | Drupal 8.4.x | Drupal API