ラテは安全の代名詞
Latteは、重大なクロスサイトスクリプティング(XSS)脆弱性に対して効果的に保護された、唯一のPHPテンプレートシステムです。これは、いわゆる文脈依存のエスケープのおかげです。話しましょう。
- XSS脆弱性の原理は何か、なぜそんなに危険なのか。
- LatteがXSSからの防御に非常に効果的なのはなぜか?
- なぜTwigやBladeなどのテンプレートが簡単に侵害されるのか?
クロスサイトスクリプティング(XSS)
クロスサイトスクリプティング(XSS)は、Web サイトにおける最も一般的な脆弱性の 1 つであり、非常に危険な脆弱性です。この脆弱性を利用することで、攻撃者は悪意のあるスクリプト(マルウェアと呼 ばれる)を外部のサイトに挿入し、疑うことを知らないユーザーのブラウザで実行させる ことができます。
このようなスクリプトは何ができるのだろうか?例えば、ログイン後に表示される機密データなど、侵害されたサイトから攻撃者に任意のコンテンツを送信することができます。また、ユーザーの代わりにページを変更したり、他のリクエストを行うことも可能です。 例えば、それがウェブメールであれば、機密性の高いメッセージを読んだり、表示内容を変更したり、設定を変更したりすることができます。例えば、すべてのメッセージのコピーを攻撃者のアドレスに転送して、今後のメールにアクセスできるようにするなどです。
XSSが最も危険な脆弱性のトップである理由もここにある。ウェブサイト上で脆弱性が発見された場合、悪用を防ぐためにできるだけ早く削除する必要があります。
脆弱性はどのように発生するのか?
このエラーは、ウェブページが生成され、変数が印刷される場所で発生します。検索ページを作成する場合、冒頭に検索語を入力する段落があると想像してください。
echo '<p>Search results for <em>' . $search . '</em></p>';
攻撃者は、以下のようなHTMLコードを含む任意の文字列を検索フィールドに書き込むことができます。
<script>alert("Hacked!")</script>
のようなHTMLコードを含む任意の文字列を検索フィールドに、したがって変数$search
に書き込むことができます。この出力はサニタイズされていないため、表示されているページの一部となります。
<p>Search results for <em><script>alert("Hacked!")</script></em></p>
ブラウザは検索文字列を出力する代わりに、JavaScriptを実行する。こうして、攻撃者はページを乗っ取ってしまうのです。
変数にコードを入れると確かにJavaScriptが実行されるが、それは攻撃者のブラウザーの中だけの話だと反論されるかもしれない。では、そのコードはどのようにして被害者の手元に届くのでしょうか。この観点から、いくつかのタイプのXSSを区別することができます。検索ページの例では、*reflected XSS*について話しています。 この場合、被害者は、パラメータに悪意のあるコードが含まれるリンクをクリックするよう、だまされる必要があります。
https://example.com/?search=<script>alert("Hacked!")</script>
リンクにアクセスさせるためには、ソーシャルエンジニアリングが必要ですが、難しいことではありません。ユーザーは、メールであれソーシャルメディアであれ、あまり考えずにリンクをクリックします。そして、アドレスに不審な点があることは、URL短縮ツールでマスクすることができるので、ユーザーはbit.ly/xxx
を見るだけです。
しかし、「stored XSS」や「persistent XSS」と呼ばれる、より危険な第二の攻撃があります。
この例として、ユーザーがコメントを投稿するWebサイトが挙げられます。攻撃者はコードを含む投稿を送り、それがサーバーに保存される。サイトの安全性が十分でない場合、そのコードはすべての訪問者のブラウザで実行されます。
この攻撃のポイントは、文字列をページに取り込むことにあるようです。
<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
のみを使用する必要があります。
JavaScript のように文字列を評価する関数については特に注意が必要で、eval()
だけでなくsetTimeout()
や、setAttribute()
をonload
などのイベント属性と一緒に使用することも必要です。しかし、これはテンプレートでカバーされる範囲を超えています。
理想的な3ポイントディフェンス。
- データが出力されているコンテキストを認識する
- そのコンテキストのルールに従ってデータをサニタイズする(すなわち「コンテキスト・アウェア」)。
- これを自動的に行う
コンテキストを考慮したエスケープ
コンテキストという言葉は一体何を意味しているのでしょうか?それは、出力するデータを扱うための独自のルールを持つ、文書内の場所のことです。これは文書の種類(HTML、XML、CSS、JavaScript、プレーンテキスト、…)によって異なり、文書の特定の部分で異なる場合があります。 例えば、HTML文書では、非常に異なるルールが適用されるそのような場所(コンテキスト)が多数存在します。その数の多さに驚かれるかもしれません。ここでは、最初の4つを紹介します。
<p>#text</p>
<img src="#attribute">
<textarea>#rawtext</textarea>
<!-- #comment -->
HTMLページの最初の、そして基本的なコンテキストは、HTMLテキストです。ここでのルールは何でしょうか。特別な意味を持つ文字<
and &
はタグやエンティティの始まりを表すので、HTMLエンティティ (<
with
<
,&
with &
)
に置き換えてエスケープする必要があります。
2番目に多い文脈は、HTML属性の値です。テキストとは異なり、ここでは属性を区切る引用符("
or '
)が特別な意味を持ちます。これは、属性の終わりとみなされないように実体として書く必要があります。
一方、<
という文字は、特別な意味を持たないので、属性の中で安全に使うことができます;タグやコメントの始まりとは理解されません。
しかし、HTMLでは引用符なしで属性値を書くことができ、その場合、すべての文字が特別な意味を持つので、別の別の文脈となることに注意しましょう。
驚かれるかも知れませんが、特殊なルールが <textarea>
と
<title>
要素の内部では特別なルールが適用されます。<
character need not (but
can) be escaped unless followed by /
。しかし、これはむしろ好奇心の問題です。
HTMLコメントの内部は面白いです。ここでは、HTMLエンティティはエスケープに使用されません。コメント内でどのようにエスケープするかを述べた仕様すらありません。 あなたはただ、やや「奇妙なルール」 に従って、コメント中の特定の文字の組み合わせを避けなければならないのです。
コンテキストは階層化することもできます。これは、JavaScriptやCSSをHTMLに埋め込むときに起こります。これは、要素または属性の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そのものに書き出す。つまり、文字列を引用符で囲むと同時に、その中に含まれる引用符を
ament
文字でエスケープするのです。
'Rock\'n\'Roll'
このコードに何かをさせるために、関数呼び出しを追加することができます。
alert('Rock\'n\'Roll');
を使って、このコードをHTML文書に挿入すると、禁止されている
のシーケンスが存在しないので、何も修正する必要がありません。
<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
そして、この文字列を属性で出力する際にも、この文脈に従ってエスケープを適用し、&
with &
を置き換えます。
<a href="https://example.org/?a=Jazz&b=Rock%27n%27Roll">
ここまで読んでくれた方、おめでとうございます!疲れましたね。さて、あなたは文脈とエスケープが何であるかについてよく理解しています。そして、それが複雑であることを心配する必要はありません。ラテが自動的にやってくれます。
LatteとNaive Systemsの比較
これまで、HTML文書で適切にエスケープする方法と、コンテキスト、すなわちデータを出力する場所を知ることがいかに重要かを紹介してきました。言い換えれば、文脈依存のエスケープがどのように機能するかということです。 これは機能的なXSS防御のための前提条件ですが、Latteはこれを行う唯一のPHP用テンプレートシステムです。
今日、すべてのシステムが自動エスケープ機能を備えていると主張しているのに、どうしてこんなことが可能なのでしょうか? 文脈を知らずに自動でエスケープするのは、誤った安心感を与えるちょっとでたらめなものです。
TwigやLaravel Bladeなどのテンプレートシステムは、テンプレートの中にあるHTML構造を一切見ません。したがって、彼らもコンテキストを見ません。Latteと比較すると、彼らは盲目で素朴です。彼らは自分自身のマークアップだけを扱い、他のすべては彼らにとっては無関係な文字列です。
░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░
░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{{ text }}░░░░░░░░░░░
░░░░░░░░░░░░░░░░░░░{{ text }}░░░░
- in text: <span>{{ text }}</span>
- in tag: <span {{ text }} ></span>
- in attribute: <span title='{{ text }}'></span>
- in unquoted attribute: <span title={{ text }}></span>
- in attribute containing URL: <a href="{{ text }}"></a>
- in attribute containing JavaScript: <img onload="{{ text }}">
- in attribute containing CSS: <span style="{{ text }}"></span>
- in JavaScriptu: <script>var = {{ text }}</script>
- in CSS: <style>body { content: {{ text }}; }</style>
- in comment: <!-- {{ text }} -->
素朴なシステムでは、< > & ' "
の文字を機械的に HTML
の実体に変換するだけです。これはほとんどの用途で有効なエスケープの方法ですが、常にそうであるとは限りません。そのため、以下に示すように、様々なセキュリティホールを検出したり、防止したりすることができません。
ラテはあなたが見ているのと同じようにテンプレートを見ています。ラテはHTMLやXMLを理解し、タグや属性などを認識します。そしてそのためにコンテクストを区別し、それに応じてデータを扱います。そのため、重要なクロスサイトスクリプティングの脆弱性に対して、実に効果的な防御を提供しています。
ライブデモ
左側がLatteのテンプレートで、右側が生成されたHTMLコードです。$text
という変数が何度も出力され、その都度、微妙に異なる文脈で出力されています。そのため、エスケープも少し違っています。テンプレートのコードは自分で編集することができます。例えば、変数の内容を変更するなど。試してみてください。
すごいでしょう!?ラテは文脈に応じたエスケープを自動で行ってくれるので、プログラマーは
- データをどのようにエスケープするか考える必要も知る必要もない
- 間違うことがない
- 忘れることがない
ラテが出力時に区別し、データの扱いをカスタマイズする文脈はこれだけではありません。これからもっと面白いケースを見ていくことにしましょう。
ナイーブシステムをハックする方法
コンテキストを区別することがいかに重要であるか、そしてなぜ素朴なテンプレートシステムは 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
属性がページの一部となり、ブラウザは画像をダウンロードした後すぐにそれを実行します。
では、同じテンプレートをラテがどう扱うか見てみましょう。
<img src={$imageFile} alt={$imageAlt}>
Latteは、あなたと同じようにテンプレートを見ます。Twigとは異なり、LatteはHTMLを理解し、変数が引用符で囲まれていない属性値として出力されることを理解しています。そのため、引用符を付加しています。攻撃者が同じキャプションを挿入すると、結果としてコードは次のようになります。
<img src="photo0145.webp" alt="foo onload=alert('Hacked!')">
LatteはXSSの防止に成功しました。
JavaScriptで変数を表示する
文脈依存のエスケープのおかげで、PHP の変数を JavaScript 内部でネイティブに使用することができます。
<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}>click here</a>
書き込みを行います。
<a href="">click here</a>
フィルタnocheck を使ってチェックをオフにすることができます。
ラテの限界
Latteはアプリケーション全体に対する完全なXSS対策ではありません。Latteを使う際にセキュリティについて考えるのをやめてしまうと不幸なことになります。 Latteの目標は、攻撃者がページの構造を変えたり、HTMLの要素や属性を改竄したりできないようにすることです。しかし、出力されるデータの内容の正しさをチェックするものではありません。また、JavaScriptの動作が正しいかどうかもチェックしません。 それはテンプレートシステムの範疇を超えています。特にユーザーが入力したデータの正しさを検証することは、信頼できないプログラマーの重要な仕事です。