Latte は安全性の代名詞
Latte は、重大な脆弱性であるクロスサイトスクリプティング(XSS)に対して実効的な保護を備えた唯一の PHP テンプレートシステムです。それを可能にしているのがコンテキストに応じたエスケープです。ここでは次のことを扱います。
- XSS 脆弱性の原理と、それがなぜこれほど危険なのか
- Latte が XSS 対策でこれほど効果的な理由
- Twig や Blade などのテンプレートで、いかに簡単にセキュリティホールが生まれてしまうか
クロスサイトスクリプティング(XSS)
クロスサイトスクリプティング(略して XSS)は、ウェブサイトで最もよく見られ、最も危険な脆弱性のひとつです。攻撃者は他人のページに悪意あるスクリプト(マルウェア)を注入でき、それが何も知らないユーザーのブラウザで実行されます。
そんなスクリプトに何ができるのでしょうか。たとえば、ログイン後に表示される機微なデータを含め、乗っ取ったページの任意の内容を攻撃者へ送信できます。ページを書き換えることも、ユーザーになりすまして別のリクエストを送ることもできます。もしそれがウェブメールなら、機微なメッセージを読んだり、表示内容を書き換えたり、設定を変えたり、たとえば全メッセージの控えを攻撃者のアドレスに転送する設定にして、以後のメールへのアクセスを手に入れたりできてしまいます。
だからこそ XSS は常に最も危険な脆弱性の上位に挙げられます。ウェブサイトに脆弱性が現れたら、悪用される前にできるだけ早く取り除かなければなりません。
脆弱性はどのように生まれるのか
問題が起きるのは、ウェブページが生成され、変数が出力される場所です。検索ページを作っていて、その冒頭に検索語を含む段落を次のように出すとしましょう。
echo '<p>Search results for <em>' . $search . '</em></p>';
攻撃者は検索ボックス、つまり $search
変数に任意の文字列を入れられます。<script>alert("Hacked!")</script> のような HTML
コードも同様です。出力が浄化されていないので、それが表示されるページの一部になります。
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
ブラウザは検索文字列を表示する代わりに JavaScript を実行します。こうして攻撃者はページを支配します。
変数にコードを入れれば JavaScript が実行されるとはいえ、それは攻撃者自身のブラウザだけではないか、と思うかもしれません。ではどうやって被害者に届くのでしょうか。この観点から XSS はいくつかの種類に分けられます。先ほどの検索の例は*反射型 XSS* と呼ばれるものです。ここでは、悪意あるコードをパラメータに含むリンクを被害者にクリックさせる必要があります。
https://example.com/?search=<script>alert("Hacked!")</script>
リンクをクリックさせるにはいくらかのソーシャルエンジニアリングが要りますが、それほど難しくはありません。ユーザーはメールでも
SNS でも、あまり考えずにリンクを踏みます。アドレスに怪しいものが含まれていることは URL
短縮サービスで隠せますし、その場合ユーザーには bit.ly/xxx としか見えません。
さらに、はるかに危険な第 2 の形態があります。*蓄積型 XSS* あるいは*持続型 XSS* と呼ばれるもので、攻撃者が悪意あるコードをサーバーに保存させ、特定のページに自動的に埋め込まれるようにします。
例としては、ユーザーがコメントを書き込むページが挙げられます。攻撃者がコードを含む投稿を送り、それがサーバーに保存されます。ページが十分に守られていなければ、そのコードはすべての訪問者のブラウザで実行されます。
攻撃の核心は <script>
という文字列をページに入れることにあるように思えるかもしれません。実際には、JavaScript を差し込む方法
はいくらでもあります。HTML
属性を使う例を見てみましょう。画像に説明文を付けられるフォトギャラリーがあり、その説明文が
alt 属性に表示されるとします。
echo '<img src="' . $imageFile . '" alt="' . $imageAlt . '">';
攻撃者は説明文として巧妙に作った文字列 " onload="alert('Hacked!')
を入れるだけでよく、出力が浄化されていなければ、できあがるコードは次のようになります。
<img src="photo0145.webp" alt="" onload="alert('Hacked!')">
注入された onload
属性がページの一部になりました。ブラウザは画像を読み込むとすぐに、その中のコードを実行します。乗っ取られたわけです。
XSS からどう身を守るか
<script>
という文字列をブロックするようなブラックリストで攻撃を検出しようとしても不十分です。実効的な防御の土台は、ページに出力するすべてのデータを一貫して浄化することです。
その中心は、特別な意味を持つ文字をすべて対応する並びに置き換えることで、これを俗にエスケープと呼びます(並びの最初の文字をエスケープ文字と呼ぶことに由来します)。たとえば
HTML テキストでは <
という文字が特別な意味を持つので、タグの始まりと解釈させたくないなら、見た目が対応する並び、すなわち
HTML エンティティ <
に置き換えなければなりません。そうすればブラウザは小なり記号を表示します。
データがどの文脈で出力されるかを区別することが決定的です。 文脈が違えば文字列の浄化の仕方も違うからです。文脈ごとに特別な意味を持つ文字が異なります。たとえば HTML テキスト、HTML 属性、一部の特別な要素の中などで、エスケープは異なります。これについてはすぐあとで詳しく扱います。
浄化は、文字列がページに出力されるまさにその瞬間に行うのが最善です。そうすれば確実に、しかもちょうど一度だけ行われます。そして浄化はテンプレートシステム自身が自動で担当するのが理想です。自動でなければ、プログラマーが忘れるかもしれません。そして一度の見落としが、ウェブサイトを脆弱にします。
とはいえ XSS
はテンプレートでのデータ出力だけの話ではなく、信頼できないデータを正しく扱わなければならないアプリケーションのほかの部分にも関わります。たとえばアプリケーションの
JavaScript は、信頼できないデータを扱うときに innerHTML ではなく innerText や
textContent を使う必要があります。eval() のように文字列を JavaScript
として評価する関数、さらに setTimeout()、onload のようなイベント属性への
setAttribute()
の使用などにも特に注意が必要です。ただしこれはテンプレートが扱う範囲を超えています。
理想的な防御の 3 か条:
- データが出力される文脈を認識する。
- その文脈の規則に従ってデータを浄化する(つまり「コンテキストに応じた」処理)。
- それを自動で行う。
コンテキストに応じたエスケープ
文脈という言葉は正確には何を指すのでしょうか。それは、出力されるデータの扱いに独自の規則を持つ、ドキュメント内の場所のことです。ドキュメントの種類(HTML、XML、CSS、JavaScript、プレーンテキストなど)によって決まり、特定の部分では異なることもあります。たとえば HTML ドキュメントには、まったく異なる規則が当てはまる場所(文脈)がたくさんあります。その多さに驚くかもしれません。まずは 4 つ挙げます。
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
HTML ページの既定にして基本の文脈は HTML
テキストです。ここでの規則はどうでしょうか。< と &
の文字が特別な意味を持ち、タグやエンティティの始まりを表すので、HTML
エンティティに置き換えてエスケープしなければなりません(< は <
に、& は & になります)。
2 番目に多い文脈は HTML 属性の値です。テキストと違うのは、属性を囲む引用符 " や
'
がここでは特別な意味を持つ点です。属性の終わりと解釈されないよう、エンティティとして書く必要があります。逆に
<
の文字は属性の中では特別な意味を持たないので安全に使えます。タグやコメントの始まりと解釈されることはありません。ただし注意してください。HTML
では属性値を引用符なしで書くこともでき、その場合はさまざまな文字が特別な意味を持つので、これはまた別の文脈になります。
意外かもしれませんが、<textarea> と <title>
要素の中では特別な規則が当てはまり、< のあとに /
が続かない限り、この文字はエスケープする必要がありません(してもかまいません)。とはいえ、これは細かい話です。
面白くなるのは HTML コメントの中です。ここではエスケープに HTML エンティティを使いません。実のところ、コメントの中でどうエスケープすべきかを定めた仕様はありません。少々 変わった規則 に従い、その中で特定の文字の並びを避ける必要があるだけです。
文脈は入れ子にもなります。HTML の中に JavaScript や CSS を埋め込むときがそうです。これは要素を使う方法と属性を使う方法の 2 通りがあります。
<script>#js-element</script>
<img onclick="#js-attribute">
<style>#css-element</style>
<p style="#css-attribute"></p>
2 つの経路と、2 つの異なるエスケープの仕方です。<script> と
<style> 要素の中では、HTML コメントと同じく HTML
エンティティによるエスケープは行われません。これらの要素の中にデータを出力するときに守るべき規則はひとつだけで、テキストがそれぞれ
</script や </style という並びを含まないことです。
逆に style 属性や on*** 属性の中では、HTML
エンティティを使ってエスケープします。
そしてもちろん、入れ子になった JavaScript や CSS
の内部では、それぞれの言語のエスケープ規則が当てはまります。ですから onload
のような属性の中の文字列は、まず JS の規則で、次に HTML
属性の規則でエスケープされます。
やれやれ。ご覧のとおり HTML は文脈が入れ子になった非常に複雑なドキュメントで、データをどこに出力しているのか(つまりどの文脈にいるのか)を正確に把握しないことには、正しいやり方は決まりません。
例が必要ですか?
Rock'n'Roll という文字列を取り上げましょう。
これを HTML テキストに出力する場合、この文字列には特別な意味を持つ文字が含まれていないので、この例に限っては置き換えは不要です。単引用符で囲まれた HTML 属性の中に出力するなら話は変わります。その場合は引用符を HTML エンティティにエスケープする必要があります。
<div title='Rock'n'Roll'></div>
ここまでは簡単でした。もっと面白くなるのは文脈が入れ子になる場合、たとえばその文字列が JavaScript の一部になるときです。
まずは JavaScript
の中に出力してみましょう。つまり引用符で囲み、同時にその中に含まれる引用符を \
でエスケープします。
'Rock\'n\'Roll'
コードが何かをするように、関数呼び出しを足すこともできます。
alert('Rock\'n\'Roll');
このコードを <script> を使って HTML ドキュメントに入れる場合、禁止された並び
</script を含まないので、これ以上の変更は不要です。
<script> alert('Rock\'n\'Roll'); </script>
一方、HTML 属性の中に入れたいなら、やはり引用符を HTML エンティティにエスケープする必要があります。
<div onclick='alert('Rock\'n\'Roll')'></div>
入れ子になる文脈は JS や CSS だけとは限りません。URL もよくある例です。URL
のパラメータは、特別な意味を持つ文字を %
で始まる並びに変換してエスケープします。例を挙げます。
https://example.org/?a=Jazz&b=Rock%27n%27Roll
そしてこの文字列を属性に出力するときは、さらにその文脈に従ってエスケープし、&
を & に置き換えます。
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
ここまで読んでくださったなら、おめでとうございます。なかなか骨の折れる内容でした。これで文脈とエスケープが何なのか、しっかり理解できたはずです。そして複雑さを心配する必要はありません。Latte がこれを自動でやってくれます。
Latte と素朴なシステム
HTML ドキュメントで正しくエスケープする方法と、文脈、つまりデータを出力する場所を知ることがいかに決定的かを見てきました。言い換えれば、コンテキストに応じたエスケープがどう働くかを見てきたわけです。これは XSS に対する実効的な防御に欠かせない前提ですが、それができる PHP テンプレートシステムは Latte だけです。
今日どのシステムも自動エスケープを備えていると謳っているのに、どうしてそうなるのでしょうか。文脈を知らない自動エスケープはいささかまやかしで、誤った安心感を生みます。
Twig や Laravel Blade などのテンプレートシステムは、テンプレートの中に HTML の構造を見ていません。ですから文脈も見えていません。Latte と比べれば、目が見えず素朴なのです。自分のタグだけを処理し、それ以外はすべて意味のない文字の流れにすぎません。
░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- テキスト内: <span>{{ foo }}</span>
- タグ内: <span {{ foo }} ></span>
- 属性内: <span title='{{ foo }}'></span>
- 引用符なしの属性内: <span title={{ foo }}></span>
- URL を含む属性内: <a href="{{ foo }}"></a>
- JavaScript を含む属性内: <img onload="{{ foo }}">
- CSS を含む属性内: <span style="{{ foo }}"></span>
- JavaScript 内: <script>var = {{ foo }}</script>
- CSS 内: <style>body { content: {{ foo }}; }</style>
- コメント内: <!-- {{ foo }} -->
素朴なシステムは、< > & ' " の文字を機械的に HTML
エンティティに変換するだけです。それは多くの場面で有効なエスケープ方法ではありますが、常に十分とは到底いえません。ですからこのあと示すように、さまざまなセキュリティホールの発生を検出することも防ぐこともできません。
Latte はあなたと同じようにテンプレートを見ています。HTML と XML を理解し、タグや属性などを認識します。そのおかげで個々の文脈を区別し、データをそれに応じて扱えます。こうして、重大な脆弱性であるクロスサイトスクリプティングに対して本当に実効的な保護を提供するのです。
░░░░░░░░░░░<span>{$foo}</span>
░░░░░░░░░░<span {$foo} ></span>
░░░░░░░░░░░░░░░░<span title='{$foo}'></span>
░░░░░░░░░░░░░░░░░░░░░░░░░<span title={$foo}></span>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<a href="{$foo}"></a>
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<img onload="{$foo}">
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░<span style="{$foo}"></span>
░░░░░░░░░░░░░░░░░░<script>░░░░░░{$foo}</script>
░░░░░░░░░░<style>░░░░░░░░░░░░░░░░{$foo}░░░</style>
░░░░░░░░░░░░░░<!--░{$foo}░-->
- テキスト内: <span>{$foo}</span>
- タグ内: <span {$foo} ></span>
- 属性内: <span title='{$foo}'></span>
- 引用符なしの属性内: <span title={$foo}></span>
- URL を含む属性内: <a href="{$foo}"></a>
- JavaScript を含む属性内: <img onload="{$foo}">
- CSS を含む属性内: <span style="{$foo}"></span>
- JavaScript 内: <script>var = {$foo}</script>
- CSS 内: <style>body { content: {$foo}; }</style>
- コメント内: <!-- {$foo} -->
ライブデモ
左に見えるのが Latte のテンプレート、右が生成された HTML コードです。変数 $text
は何度も出力されていますが、そのたびに少しずつ違う文脈にあります。ですからエスケープの仕方も少しずつ違います。テンプレートのコードは自分で編集できます。たとえば変数の内容を変えてみてください。試してみましょう。
素晴らしいと思いませんか。Latte はコンテキストに応じたエスケープを自動で行うので、プログラマーは次のようになります。
- どこでどうエスケープするかを考えたり知ったりする必要がない
- 間違えようがない
- エスケープを忘れようがない
これでも、Latte が出力時に区別しデータの扱いを変える文脈のすべてではありません。ここからは、ほかの興味深いケースを見ていきましょう。
素朴なシステムをどう破るか
いくつかの実践的な例を通して、文脈の区別がどれほど重要か、そして Latte と違って素朴なテンプレートシステムが XSS に対する十分な保護を提供しない理由を示します。例では素朴なシステムの代表として Twig を使いますが、ほかのシステムにも同じことが当てはまります。
属性による脆弱性
先ほど示したように、HTML 属性を使ってページに悪意あるコードを注入してみましょう。画像を描画する Twig のテンプレートがあるとします。
<img src={{ imageFile }} alt={{ imageAlt }}>
属性値の周りに引用符がないことに注目してください。コーダーが忘れただけかもしれませんし、そういうことは起こります。たとえば React では引用符なしでコードを書くので、言語を行き来するコーダーは簡単に引用符を忘れてしまいます。
攻撃者は画像の説明文として巧妙に作った文字列 foo onload=alert('Hacked!')
を入れます。Twig が、変数が HTML
テキストの流れの中で出力されているのか、属性の中なのか、HTML
コメントの中なのかを判断できないこと、要するに文脈を区別しないことはすでに分かっています。Twig
はただ機械的に < > & ' " の文字を HTML
エンティティに変換します。ですからできあがるコードは次のようになります。
<img src=photo0145.webp alt=foo onload=alert('Hacked!')>
セキュリティホールができてしまいました。
偽装された onload
属性がページの一部になり、ブラウザは画像を読み込んだ直後にそれを実行します。
では、同じテンプレートを Latte がどう扱うか見てみましょう。
<img src={$imageFile} alt={$imageAlt}>
Latte はあなたと同じようにテンプレートを見ています。Twig と違って HTML を理解し、変数が引用符で囲まれていない属性の値として出力されていることを知っています。ですから引用符を補います。攻撃者が同じ説明文を入れても、できあがるコードは次のようになります。
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
Latte は XSS をきちんと防ぎました。
JavaScript での変数の出力
コンテキストに応じたエスケープのおかげで、JavaScript の中で PHP の変数を自然に使えます。
<p onclick="alert({$movie})">{$movie}</p>
<script>var movie = {$movie};</script>
変数 $movie に文字列 'Amarcord & 8 1/2'
が入っていれば、次の出力が生成されます。HTML の中と JavaScript
の中でエスケープが異なり、onclick
属性ではさらに別のエスケープになっていることに注目してください。
<p onclick="alert("Amarcord & 8 1/2")">Amarcord & 8 1/2</p>
<script>var movie = "Amarcord & 8 1/2";</script>
リンクのチェック
Latte は、src、href、action、formaction、<object>
要素の data といった URL 属性で使われる変数が安全な URL
を含んでいるかを自動的に確認します。一般的なプロトコル(http、https、ftp、mailto、tel、sms)と相対
URL を許可し、javascript: のような危険になり得るものを遮ります。
{var $link = 'javascript:attack()'}
<a href={$link}>ここをクリック</a>
出力:
<a href="">ここをクリック</a>
このチェックは nocheck フィルタで無効にできます。
クライアントサイドフレームワーク
Vue や Angular
のようなフレームワークは、ページの一部を自分のテンプレートとして扱い、その中の二重波かっこ
{{ ... }}
を式として解釈します。攻撃者がそのような並びを出力される変数に混ぜ込めれば、サーバー側では
XSS
が起きていなくても、ページの読み込み後にブラウザでフレームワークがそれを評価してしまいます。
Latte はこれを防ぎます。HTML テキストに出力するとき、{{ の組を
{<!-- -->{
に変換します。空のコメントは読者には見えませんが、フレームワークが自分のタグだと認識するのを防ぎます。それ以外の
{ の出現はエンティティ {
に置き換えられます(属性値の中では、すべての {
がこの方法でエスケープされます)。
{var $query = 'Hi {{ constructor.constructor("alert(1)")() }}'}
<h1>Search: {$query}</h1>
出力:
<h1>Search: Hi {<!-- -->{ constructor.constructor("alert(1)")() }}</h1>
この保護は自動的に働き、設定は不要です。
Latte の限界
Latte はアプリケーション全体にとっての完全な XSS 対策ではありません。Latte を使っているからといってセキュリティを考えなくなってしまうのは、私たちの本意ではありません。Latte の目的は、攻撃者がページの構造を変えたり、HTML の要素や属性を偽造したりできないようにすることです。しかし出力されるデータの内容が正しいかどうかは確認しません。JavaScript の振る舞いの正しさも同様です。それはテンプレートシステムの守備範囲を超えています。データ、とりわけユーザーが入力した信頼できないデータの正しさを検証するのは、プログラマーの重要な仕事です。
個々の文脈でのエスケープについて、自分の知識を試してみたいですか? XSS 脆弱性クイズに挑戦してみてください。