Latteはセキュリティの代名詞
Latteは、重大な脆弱性であるクロスサイトスクリプティング(XSS)に対する効果的な保護を備えた唯一のPHP用テンプレートシステムです。これは、いわゆるコンテキストに応じたエスケープのおかげです。以下について説明します。
- XSS脆弱性の原理と、なぜそれがそれほど危険なのか
- LatteがXSSに対する防御においてなぜそれほど効果的なのか
- Twig、Bladeなどのテンプレートでセキュリティホールを簡単に作成する方法
クロスサイトスクリプティング (XSS)
クロスサイトスクリプティング(略してXSS)は、Webページの最も一般的な脆弱性の1つであり、非常に危険です。攻撃者は、悪意のあるスクリプト(いわゆるマルウェア)を他人のページに挿入し、それを何も知らないユーザーのブラウザで実行させることができます。
そのようなスクリプトは何ができるのでしょうか?例えば、攻撃されたページから任意のコンテンツを攻撃者に送信できます。これには、ログイン後に表示される機密データも含まれます。ページを変更したり、ユーザーに代わって他のリクエストを実行したりすることもできます。 例えば、それがWebメールの場合、機密メッセージを読み取ったり、表示されるコンテンツを変更したり、設定を再構成したりできます。例えば、将来のメールにもアクセスできるように、すべてのメッセージのコピーを攻撃者のアドレスに転送するように設定するなどです。
そのため、XSSは最も危険な脆弱性のランキングで上位に位置しています。Webページで脆弱性が発見された場合、悪用を防ぐためにできるだけ早く削除する必要があります。
脆弱性はどのように発生しますか?
エラーは、Webページが生成され、変数が出力される場所で発生します。検索ページを作成していて、最初に検索語を含む段落が次のような形式であると想像してください:
echo '<p>Výsledky vyhledávání pro <em>' . $search . '</em></p>';
攻撃者は、検索ボックス、ひいては変数$search
に任意の文字列、つまり<script>alert("Hacked!")</script>
のようなHTMLコードを入力できます。出力がサニタイズされていないため、表示されるページの一部になります:
<p>Výsledky vyhledávání pro <em><script>alert("Hacked!")</script></em></p>
ブラウザは、検索文字列を出力する代わりにJavaScriptを実行します。そして、攻撃者がページを支配します。
変数にコードを挿入するとJavaScriptが実行されるが、それは攻撃者のブラウザでのみであると反論するかもしれません。どのようにして標的に到達するのでしょうか?この観点から、いくつかのタイプのXSSを区別します。検索の例では、反射型XSSについて話しています。 ここでは、パラメータに悪意のあるコードを含むリンクをクリックするように標的を誘導する必要があります:
https://example.com/?search=<script>alert("Hacked!")</script>
リンクにユーザーを誘導するには、ある程度のソーシャルエンジニアリングが必要ですが、それほど複雑ではありません。ユーザーは、Eメールやソーシャルネットワーク上のリンクを、あまり考えずにクリックします。そして、アドレスに何か疑わしいものがあることは、URL短縮サービスを使用して隠すことができ、ユーザーはbit.ly/xxx
しか見ません。
しかし、格納型XSSまたは持続型XSSと呼ばれる、より危険な攻撃形式も存在します。この場合、攻撃者は悪意のあるコードをサーバーに保存し、それが一部のページに自動的に挿入されるようにします。
例としては、ユーザーがコメントを書き込むページがあります。攻撃者はコードを含む投稿を送信し、それがサーバーに保存されます。ページが十分に保護されていない場合、すべての訪問者のブラウザで実行されます。
攻撃の核心は、文字列<script>
をページに入れることにあるように思われるかもしれません。実際には、XSS_Filter_Evasion_Cheat_Sheet.html。
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属性、一部の特殊な要素内などでのエスケープは異なります。これについては後で詳しく説明します。
サニタイズは、ページに文字列を出力する際に直接行うのが最善です。これにより、それが確実に実行され、一度だけ実行されることが保証されます。サニタイズが自動的にテンプレートシステムによって行われるのが最善です。 なぜなら、サニタイズが自動的に行われない場合、プログラマーがそれを忘れる可能性があるからです。そして、1つの見落としは、Webが脆弱であることを意味します。
しかし、XSSはテンプレートでのデータの出力だけでなく、信頼できないデータを正しく処理する必要があるアプリケーションの他の部分にも関係します。例えば、アプリケーションのJavaScriptが、それらに関連してinnerHTML
を使用せず、innerText
またはtextContent
のみを使用することが不可欠です。
文字列をJavaScriptとして評価する関数(eval()
だけでなく、setTimeout()
や、onload
などのイベント属性でのsetAttribute()
の使用など)には特に注意が必要です。しかし、これはテンプレートがカバーする領域を超えています。
3つのポイントでの理想的な防御:
- データが出力されるコンテキストを認識する
- そのコンテキストのルールに従ってデータをサニタイズする(つまり、「コンテキストに応じて」)
- それを自動的に行う
コンテキストに応じたエスケープ
コンテキストという言葉で正確には何を意味しますか?それは、出力されるデータのサニタイズに関する独自のルールを持つドキュメント内の場所です。それはドキュメントのタイプ(HTML、XML、CSS、JavaScript、プレーンテキストなど)に依存し、その特定の場所によって異なる場合があります。 例えば、HTMLドキュメントには、非常に異なるルールが適用される多くの場所(コンテキスト)があります。いくつあるか驚くかもしれません。ここに最初の4つがあります:
<p>#テキスト</p>
<img src="#属性">
<textarea>#rawtext</textarea>
<!-- #コメント -->
HTMLページのデフォルトかつ基本的なコンテキストはHTMLテキストです。ここでのルールは何ですか?特別な意味を持つ文字は<
と&
で、これらはタグまたはエンティティの開始を表すため、HTMLエンティティに置き換えてエスケープする必要があります(<
は<
に、&
は&
に)。
2番目に一般的なコンテキストはHTML属性の値です。テキストとの違いは、ここでは属性を囲む引用符"
または'
が特別な意味を持つことです。属性の終わりとして解釈されないように、エンティティで記述する必要があります。
逆に、属性内では<
文字を安全に使用できます。なぜなら、ここでは特別な意味を持たず、タグやコメントの開始として解釈されることはないからです。
しかし注意してください。HTMLでは属性値を引用符なしで記述することもできます。その場合、多くの文字が特別な意味を持つため、これは別の独立したコンテキストになります。
驚くかもしれませんが、<textarea>
および<title>
要素内では特別なルールが適用され、<
文字の後に/
が続かない限り、エスケープする必要はありません(ただし、エスケープすることもできます)。しかし、これはどちらかというと豆知識です。
HTMLコメント内では興味深いです。ここでは、エスケープにHTMLエンティティは使用されません。実際、どの仕様もコメント内でどのようにエスケープすべきかを規定していません。 ただ、ややsyntax.htmlに従い、特定の文字の組み合わせを避ける必要があります。
コンテキストはネストすることもできます。これは、JavaScriptまたはCSSをHTMLに埋め込むときに発生します。これは2つの異なる方法、要素と属性で行うことができます:
<script>#js-element</script>
<img onclick="#js-atribut">
<style>#css-element</style>
<p style="#css-atribut"></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 vs ナイーブなシステム
HTMLドキュメントで正しくエスケープする方法と、データを出力する場所、つまりコンテキストの知識がいかに重要であるかを示しました。言い換えれば、コンテキストに応じたエスケープがどのように機能するかです。 これはXSSに対する機能的な防御の不可欠な前提条件ですが、Latteはこれを実行できるPHP用の唯一のテンプレートシステムです。
今日のすべてのシステムが自動エスケープを備えていると主張しているのに、どうしてそうなのでしょうか? コンテキストの知識のない自動エスケープは、安全であるという誤った印象を与える、少しくだらないものです。
Twig、Laravel Bladeなどのテンプレートシステムは、テンプレート内にHTML構造をまったく見ていません。したがって、コンテキストも見ていません。Latteと比較して、それらは盲目でナイーブです。それらは独自のタグのみを処理し、他のすべてはそれらにとって重要でない文字の流れです:
░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░
░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░{{ foo }}░░░░
- v textu: <span>{{ foo }}</span>
- v tagu: <span {{ foo }} ></span>
- v atributu: <span title='{{ foo }}'></span>
- v atributu bez uvozovek: <span title={{ foo }}></span>
- v atributu obsahujícím URL: <a href="{{ foo }}"></a>
- v atributu obsahujícím JavaScript: <img onload="{{ foo }}">
- v atributu obsahujícím CSS: <span style="{{ foo }}"></span>
- v JavaScriptu: <script>var = {{ foo }}</script>
- v CSS: <style>body { content: {{ foo }}; }</style>
- v komentáři: <!-- {{ 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}░-->
- v textu: <span>{$foo}</span>
- v tagu: <span {$foo} ></span>
- v atributu: <span title='{$foo}'></span>
- v atributu bez uvozovek: <span title={$foo}></span>
- v atributu obsahujícím URL: <a href="{$foo}"></a>
- v atributu obsahujícím JavaScript: <img onload="{$foo}">
- v atributu obsahujícím CSS: <span style="{$foo}"></span>
- v JavaScriptu: <script>var = {$foo}</script>
- v CSS: <style>body { content: {$foo}; }</style>
- v komentáři: <!-- {$foo} -->
ライブデモ
左側にはLatteのテンプレート、右側には生成されたHTMLコードが表示されます。変数$text
が数回出力され、毎回少し異なるコンテキストで出力されます。したがって、エスケープも少し異なります。テンプレートコードは自分で編集できます。例えば、変数の内容を変更するなどです。試してみてください:
素晴らしいでしょう!Latteはコンテキストに応じたエスケープを自動的に行うので、プログラマーは:
- どこでどのようにエスケープするかを考えたり知ったりする必要がない
- 間違えることがない
- エスケープを忘れることがない
これらは、Latteが出力時に区別し、データのサニタイズを調整するすべてのコンテキストではありません。さらに興味深いケースを今から見ていきましょう。
ナイーブなシステムをハックする方法
いくつかの実践的な例を通して、コンテキストの区別がいかに重要であり、ナイーブなテンプレートシステムがLatteとは異なり、XSSに対する十分な保護を提供しない理由を示します。 ナイーブなシステムの代表として、例ではTwigを使用しますが、他のシステムにも同じことが当てはまります。
属性による脆弱性
上記で示したように、HTML属性を使用して悪意のあるコードをページに注入しようとします。画像をレンダリングするTwigのテンプレートがあるとします:
<img src={{ imageFile }} alt={{ imageAlt }}>
属性値の周りに引用符がないことに注意してください。コーダーがそれらを忘れた可能性があります。これは単に起こることです。例えば、Reactではコードはこのように引用符なしで書かれ、言語を切り替えるコーダーは簡単に引用符を忘れる可能性があります。
攻撃者は、画像の説明として巧妙に作成された文字列foo onload=alert('Hacked!')
を挿入します。Twigは、変数がHTMLテキストの流れの中、属性内、HTMLコメント内など、どこに出力されているかを判断できないこと、つまりコンテキストを区別しないことをすでに知っています。そして、文字< > & ' "
を機械的に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
属性で使用される変数がWeb
URL(つまりHTTPプロトコル)を含んでいるかどうかを自動的にチェックし、セキュリティリスクをもたらす可能性のあるリンクの出力を防ぎます。
{var $link = 'javascript:attack()'}
<a href={$link}>クリック</a>
出力:
<a href="">クリック</a>
チェックはnocheckフィルタを使用して無効にできます。
Latteの制限
Latteは、アプリケーション全体に対するXSSからの完全な保護ではありません。Latteを使用する際にセキュリティについて考えるのをやめてほしくありません。 Latteの目標は、攻撃者がページの構造を変更したり、HTML要素や属性を偽装したりできないようにすることです。しかし、出力されるデータのコンテンツの正確性や、JavaScriptの動作の正確性はチェックしません。 これはテンプレートシステムの能力を超えています。データの正確性、特にユーザーによって挿入された信頼できないデータの検証は、プログラマーの重要なタスクです。