[クライアントサイド〜サーバーサイド] テンプレートエンジンでのセキュリティ的な問題や考え方
この記事は次のスライドの文字起こし的な内容です。
スライドの画像 + 喋った内容のNote的なものをそれぞれのページごとに書き込んでいます。 リンクとかはスライド版ならクリックできるので、そっちを見るといいのかもしれません。
画像だらけなので、画像が読み込み終わるのを待つといい気がします。
クライアントサイドからサーバサイドまで破壊するテンプレートエンジンを利用した攻撃と対策
テンプレートエンジンに関するセキュリティ的な問題についてのお話です。
テンプレートだからエンドユーザー(サービスの利用者)に書かせて安全だと思っていても、選んだテンプレートエンジンの性質によっては安全ではない場合があります。
また、JavaScriptのテンプレートエンジンはクライアントサイド(Browser)とサーバサイド(Node.js)どちらもで動かせるものが多いです。 そのため、クライアントサイドとサーバーサイド両方に関する問題の事例などを紹介します。
自己紹介です。
このスライドで話すことです。
まずはテンプレート(エンジン)とはなにかという話をします。 その次に、テンプレートにはいくつかの種類があり、それぞれが安全に扱える場所と安全に扱わないと起きる問題について話します。 またテンプレートは一種のプログラミング言語であるということを見ていきます。
今日覚えておきたいことは次の3つです
- テンプレートエンジンには3つのレベルがある
- テンプレートエンジンのレベルとテンプレートをコンパイルする場所によって必要なセキュリティ対策は異なる
- テンプレートファイルはプログラムファイルである
Note: エンドユーザーがテンプレートを書いて保存することで、そのテンプレートがサーバでコンパイルされた際に任意のコードを実行できる脆弱性であるサーバサイドテンプレートインジェクションのデモをしました。
ここでは、サーバサイドでテンプレートエンジンを使ってコンパイルした結果をHTMLとして出力サービスをデモサイトに使いました。
そのデモサイトで、実行しているNode.jsから process.env
(環境変数) をHTMLに出力して抜き出したり、process.exit(1)
を実行させてデモサイトのプロセスを落としたりしました。
今回扱うテンプレートエンジンについてです。
ここでは、テンプレートとデータモデル(パラメータ)を組み合わせてViewを作る処理を行うものをテンプレートエンジンと呼びます。
これは主にWeb template system(HTMLを出力する)を扱うため、このような定義にしています。
このスライドでは、テンプレートエンジンを3種類に分けました。
この3つのレベルは、テンプレートエンジンに書くテンプレートの構文の制約で分けています。
テンプレートエンジンを次のような3つのレベルに分けています。
- Simple Token Replacement
- 単純な置換:
This is {{ name }}.
=>This is pen.
- 単純な置換:
- Limited Control Structures
- if文やfor文など制御構文が付いたもの
- Programming Language
- プログラミング言語みたいなもの
具体的なテンプレートエンジンがどれに該当するかは後ほど紹介します。 まずは、それぞれのテンプレートエンジンの種類を紹介します。
まずは、一番単純で制約も強い Simple Token Replacement からです。
Simple Token Replacementはテンプレートにロジックらしいロジックは書くことができません。
String#replace
で実装されているようなテンプレートエンジンと考えて問題ありません。
具体的な実装を見てみたい人は、実際に書いてみたものもあるので次のライブラリを参照してください。
次は、ある種制限された構文も使えるようになったLimited Control Structuresです。
Simple Token Replacementの上位互換と考えて問題ありません。
Limited Control Structuresでは、テンプレートに if や for文のような一種の制御構文が利用できます。
あくまで表示に関する制御構文であるため、logic less templateと呼ばれることがあります(具体的にはliquidなど)
これは、ビジネスロジックは実装できないという意味でlogic lessなんだと思います。
このテンプレートエンジンの特徴としては、エンドユーザー(サービスの利用者)に書かせても任意のコード実行はできないような作りを目指しているというところにあります。
そのため、文字列からコードを実行するeval
を原理的に使わずに書いてたり、eval
をさせないことを目的にしたテンプレートエンジンがLimited Control Structuresに該当します。
最後にProgramming Language Templateです。
Programming Language Templateは実際にただのプログラミング言語と同等レベルな高機能なテンプレートエンジンです。
Limited Control Structuresから制約を外したテンプレートエンジンと考えれば問題ありません。
このテンプレートエンジンには制約があるようでないため、エンドユーザー(サービスの利用者)に書かせるには向いていません。
エンドユーザーに書かせると、そのサーバで任意のコードが実行できるためです(これをServer Side Template Injectionとよびます)
そのため、Programming Language Templateをエンドユーザーに書かせると、サーバでコードを実行できるプラットフォームを提供しているのと同じとなります。
Programming Language Templateがどういうときに使われるかというと、開発者がウェブサイトを開発する際のテンプレートエンジンとして利用されています。
これは開発者自体は任意のコードをサービスで動かせるので、テンプレートエンジンも自由度の高いものを使うことで効率的に開発できる機能を備えているものがProgramming Language Templateに多いためです。
Web template systemからの引用ですが、PHPもProgramming Language Templateから進化したものと考えていいはずです。
ejs、pug、JSX、Vue Template、Angular TemplateなどはProgramming Language Templateです。
具体的にそれぞれのテンプレートの特徴とよくある構文を見てみます。
- Simple Token Replacement
- 制約が厳しくシンプル
- 構文はシンプルな置換のみ
- Limited Control Structures
- ここは中間なのでテンプレートエンジンの頑張り次第
- 構文はよくあるテンプレート構文が多い
- Programming Language Template
- とても自由度が高いので、何でもできます(意図してない場合もある)
- 構文は言語に近い場合もあります
具体的にそれぞれのテンプレートと実装のライブラリについてです。
具体的なライブラリを分けると次のようになると思います。
- Simple Token Replace
- Limited control structures
- Liquid, Handlebars, StringsTemplate
- Programming Language Template
- Nunjucks, Lodash, template, Pug, React, Angular, Vue…
ここまで紹介したテンプレートのレベルは
Simple Token Replacementほど制約が強く厳密です。 一方でProgramming Language Templateほど自由度が高く柔軟です。
このようにテンプレートエンジンとくくりでも、レベルによってかなり機能や自由度の幅があります。
そのため、テンプレートエンジンのレベルごとにどのような攻撃が起こり得るかについて話していきます。
テンプレート + データ を テンプレートエンジン が処理して View(HTMLなど) を作るというのが大まかな流れです。
最初に話してたように、テンプレートをテンプレートエンジンがコンパイルする場所は何箇所かがあります。
特にJavaScriptのテンプレートエンジンでは、ローカルとサーバに加えて、ブラウザ上でもテンプレートをコンパイルできます。
そのため、ここで考えるテンプレートをコンパイルする場所は次の3箇所となります。
- ローカル
- ブラウザ
- サーバ
それぞれの入力ソースとコンパイル場所とViewが生成されるタイミングの図です。
たとえば、一番上のローカルでコンパイルするケースというのは、テンプレートをコンパイル済みのHTMLを配信するタイプです。 Jekyllなどのブログエンジンなどは、事前にコンパイルして静的なHTMLを配信します。 これがローカルです。
真ん中のブラウザでのコンパイルは、ブラウザからテンプレートファイルをリクエストします。 そして取得してテンプレートをブラウザ上のJavaScriptでコンパイルします。 そのため、サーバはテンプレートそのものをレスポンスとして返すだけです。
最後のサーバは、よくあるウェブサービスです。 エンドユーザーがリクエストすると、そのリクエストに基づいてサーバにあるテンプレートをサーバ上でコンパイルします。 その生成物であるHTMLだけをレスポンスとして返します。
ここでは少し話を整理していきます。
このスライドはテンプレートの安全性についての話でした。
その安全性は
入力ソース xコンパイルする場所 x テンプレートのレベル
で大まかに決まってきます。 安全性というより、気をつけるポイントの数というのが合ってるかもしれません、
ここで振り返り。
入力ソースは、テンプレートエンジンに与えるユーザー入力です。
つまり入力ソースとなり得るのは次の2つです。
- テンプレート
- データ
入力ソース = ユーザー入力と言い換えていいかもしれません。
まずは入力ソースがデータだけの状況についてです。
入力ソース = ユーザー入力としてデータを受け取ります。 基本的にユーザー入力はどんな悪意ある文字列が入ってるかわからないので信用できない入力ソースとして扱います。
一方でこのときに、テンプレートそのものは開発者が書いたもので信用できるという状況です。
このときにそれぞれのテンプレートエンジンごとに、安全に扱えるかどうかの表です。
ユーザー入力がデータで、テンプレート自体は信用できる状態なら、どのテンプレートエンジンも安全に扱えます。
これはテンプレートエンジンがデータをエスケープするなど安全に扱う方法を持っているのが一般的であるためです。
Simple Token Replacementが単純な置換だと問題はあるかもしれませんが…
具体的な例です。
データ(モデル)に何が入ってても、出力時はエスケープできているので問題なさそうです。
この例だとHTML文字列をデータとして受け取っても、エンプレートエンジンがデータをHTMLエスケープしてから出力するので、XSSにはならないという例を見ています。
テンプレートエンジンの基本的な機能として何かしらのエスケープ機能は備わっていると思います。
一番単純なHTMLエスケープから、属性値に指定できるエスケープなどテンプレートエンジンによって様々です。
データが入る場所(Context)によってエスケープ方法が変わることをContextual Escapingと呼びます。 ただし、メジャーなフレームワークはサポートしていくような方向に見えます。
- Angularは1.xからやっている
- Ember
- Reactもhref属性のjavascript: プロトコル対応など https://reactjs.org/blog/2019/08/08/react-v16.9.0.html
その他のテンプレートエンジンでもサポートしている場合があります。
ここで言いたかったのは、データモデルに信用できないデータが渡されても問題となるテンプレートエンジンは殆どないという話です。
テンプレートの主な目的なこのような利用方法なので、これで問題が起きるケースは少ないはずです。
一方で、“テンプレート”そのものが入力ソースとなるケースがあります。
“テンプレート”そのものが入力ソースとなるケースとしてよくあるのは、ユーザーにテーマを書かせる機能を提供している場合です。
たとえば、はてなブログやTumblrなどのブログはテーマ機能を提供しています。 Jekyllなどもユーザーにテンプレートそのものを書かせるテーマ機能を持っています。
このような状態では、信用できないテンプレートを安全にコンパイルできるかが安全性に関わってきます。
安全にコンパイルできないと任意のコード実行が可能になります。
同じようにテンプレートエンジンのレベルごと見ていきます。
Simple Token ReplacementとLimited Control Structuresは、信用できないテンプレートをコンパイルしても問題ない作りです。 なぜならLimited Control Structuresはユーザーにテンプレートを書かせる目的を持ったテンプレートエンジンであるから(そういう定義にした)です。 また、Simple Token Replacementはevalを使ってないテンプレートエンジンなので、問題にはならないと思います。
一方で、信用できないテンプレートをProgramming Language Templateとしてコンパイルするのが一番危険な状態です。
ユーザーに書かせたテンプレートをProgramming Language Templateとしてコンパイルすると、任意のコード実行が可能になるケースがあります。
ユーザーにJSXを書いてもらってサーバサイドレンダリングする場合、ユーザーにJadeを書かせてコンパイルする場合などが具体的に問題となるケースです。
これは、ユーザーにプログラムを書かせてサーバで実行すると同じ意味となります。
ユーザー入力としてProgramming Language Templateをローカル実行する例としてWordのマクロがあります。
マクロは任意のコード(exe)も実行できます。 そのため、メールで知らない人から受け取ったWordファイルを開いてマクロを実行すると、任意のコード実行がローカルで可能です。
ただし、Wordのマクロはだいたい許可しない限り実行されません。
ユーザー入力としてProgramming Language Templateをブラウザで実行する例として、VueやAngularJSなどのテンプレート機能を持つフレームワークがあります。
これらのフレームワークでテンプレートをブラウザで実行、つまりレンダリングするのは問題ありません。
ただし、古典的なサーバサイドでのHTML出力とSPA(Single Page Application)が混ざったサイトでは、意図せずにユーザー入力であるProgramming Language Templateをコンパイルしているケースがあります。
次のサイトは、ユーザー入力としてProgramming Language Templateを受け取ってレンダリングしてしまってるデモサイトです。
Programming Language TemplateとしてVueを利用しています。
HTMLを返すサーバ側の実装はこのようになっています。
次のようにname
パラメータにHTMLを入れてもエスケープされているので、XSSは起きないので一見安全そうです。
しかし、次のようにname
パラメータにVueのテンプレート構文を渡すと、その中身がテンプレートとして評価されてしまいます!
つまり、{{ expression }}
のように書けば、JavaScriptとして評価されるコードがレンダリングできてしまい、XSSが発生します。
Vue 2.xでは、mount時に指定されたel
の中に書いてある {{ expression }}
をVueのテンプレート構文として評価します。
サーバサイドでは、ユーザー入力から {{ expression }}
にあたるHTMLを生成して返していました。
そのため、ユーザー入力から Programming Language Template をつくってることになり、クライアントサイドでの任意のコード実行(XSS)が可能になっています。
これの対処法についてですが、そもそもテンプレートとデータモデルが混在してしまってるのが原因です。
単純にいえば、ユーザー入力からテンプレートを生成するのが問題です。
ユーザー入力はあくまでデータモデルとしてテンプレートエンジンに渡して上げれば問題ないはずです。
次のようにdata
としてユーザー入力を渡すようにして修正できました。
この問題を実際のウェブサービスで行うデモです。(報告して修正済み)
少し検索するとこのような古典的なサーバサイドレンダリング(PHP、Ruby、Pythonなどを使った)とVueやAngularなどを使ったSPA(Single Page Application)の実装を組み合わせたサイトが見つかります。
このような組み合わせの場合、ユーザー入力からうっかりテンプレートを作るようになってしまってるサイトが多いです。
たとえば、検索結果画面で、検索する単語に {{ 1+2 }}
などようにテンプレートっぽい文字列を入れて検索すると 3
と出力されるようなサイトがあります。
VueやAngularJS(1.x)はHTMLにテンプレートを埋め込む形で書くため、サーバーサイドで中途半端にHTMLを生成しているとこの問題が発生しやすように見えました。
ReactやAngular(2.x+)などは、テンプレートをビルドのフェーズでコンパイルしてしまうので、この問題が起きにくいのかもしれません。(自然とテンプレートとデータが分離される)
最後にユーザー入力をProgramming Language Templateとして受け取ってサーバーサイドでコンパイルするケースです。
何も対策しないと、ユーザーから受け取ったコードをサーバで実行するので危険です。
サーバーサイドで任意のコード実行(RCE)ができてしまいます。
テンプレートを経由してRCEすることをServer Side Template Injectionと呼ぶらしいです。
lodash.template
は Programming Language Templateです。
次のサイトはlodash.template
を使ってレンダリングして結果を返します。
このURLは修正後のものになっています(Server Side Template Injectionできちゃうので…)
実装はこのような単純なものです。
が
しかし、lodash.template
を使っているのに、escapeHTML(name)
して結果をテンプレートにしています。
つまり、ユーザー入力からテンプレートを生成しています。
lodash.template
は実質ただのJavaScriptなので、サーバーサイドで任意のコード実行ができます。
https://server-side-lodash-template-injection.azu.now.sh/?name=${JSON.stringify(process.env)}
このようにNode.jsを動かしてるprocess.env
の内容を出力することができてしまいます。
修正方法は単純です。
ユーザー入力からテンプレートを生成しないようにすることです。
何度も行ってますが、テンプレートとデータはきちんと分けて扱うようにします。
lodash.template
も <%- name %>
のように変数のプレースホルダと、データとして name
を渡す方法があります。
それを使うようにするだけです。
テンプレートエンジンをちゃんと使うだけで問題ありません。
この問題は単純ですが、結構有名なサイトでもこの問題が起きてたりします。
ユーザー入力からテンプレートを作っていないかをチェックする検査ツールなどもあります。
実際にそれぞれのテンプレートエンジンでユーザー入力からテンプレートを生成してないかをチェックするクエリの例です。
このユーザー入力をProgramming Language Template(テンプレート)として受け取るケースはテンプレートエンジンを正しく使えば問題ありません。
しかし、サービスによってユーザーがProgramming Language Templateが書ける機能を提供していることがあります。
たとえば、ブログのテーマ機能にerbでテンプレートを書けるようなサービスがあったらとても危険です。
著名なテンプレートエンジンでもユーザーに書かせる前提になっていないものは多いので、その辺は事前にチェックしましょう。
User Defined Templateと検索すると良いです。
このUser Defined TemplateがProgramming Language Template(lodash.templateやerbなど)だと対策しないと基本的に危険です。
後から対策するのはすごい難しいので、テンプレートエンジンを選ぶ段階で、3つのテンプレートのレベルを把握しておくのが重要です。
ユーザーがProgramming Language Templateを書けるサービスでのServer Side Template Injectionのデモです(報告して修正済み)
User Defined Templateとして扱って良いテンプレートエンジンは、Limited Control Structuresを扱うテンプレートエンジンです。
具体的にはShopifyやJekyllが使うliquidjs、Twigなどはユーザーが書かせてもいい作りのテンプレートエンジンとして作られています。
Programming Language TemplateのままUser Defined Templateとしたい場合についてです。
どれも基本的にコストが高いですが、次の3つの方法で一応の安全性を保てるかもしれせん。
- セキュリティレビュー
- ホワイトリストでの機能制限
- 隔離環境での実行
セキュリティレビュー方式
Wordpress は com の方はこの方式を取っています。 なぜならWordpressのテーマはPHPを書けるためです。
ホワイトリストでの機能制限
Programming Language Templateのテンプレート構文を制限する拡張機能を作る方式です。
実質的にLimited Control Structuresに落とそうという方針です。
とても大変なので、最初からLimited Control Structuresを使うのが無難です。
バリデーションの問題点として、そのテンプレートエンジンを作ってない人がバリデーションだけを作っても、抜け道をすべて潰すのが難しいという問題があります。
隔離環境での実行
パフォーマンスやリアルタイム性が問題にならないならこれを選ぶのもありかもしれません。
まとめです。
Limited Control StructuresとProgramming Language Templateを見分ける方法についてです。
多分ありません。
そのテンプレートがプログラムに見えたらProgramming Language Templateです。
ネイティブの関数とか呼べるならProgramming Language Templateです。
見分け方はないので、公式サイトや次のようなキーワードで検索してみてください。
{{テンプレート名}} Untrusted template
{{テンプレート名}} User Defeined template
色々難しく感じるかもしれませんが、言ってることは単純です。
自分でテンプレートをコンパイルしたものを配布するのはOKです。 他人から受け取ったテンプレートを自分でコンパイルすると問題があるかもしれません。
そのテンプレートがプログラムだったらと置き換えるとわかりやすいかもしれません。
考え方のまとめです。
スライドのまとめです。
- テンプレートは3つのレベルがあると考えられる
- テンプレートのレベルとコンパイルする場所によって影響度は異なる
- ローカルコンパイルとサーバコンパイルを同じものとして扱わない
- レベルを下げるのは難しいため、必要なレベルのものを最初に選択する
- Untrustedなテンプレートをコンパイルするときには制限がある状態で行う
- Sandbox、ホワイリスト、マニュアルレビュー
- テンプレートファイルはプログラムファイル
参考文献です。
おわりに
この文字起こしにはJekyllがテンプレートとして評価する文字列がでてくるので、記事中に意図しない表示になっている場合があります。
お知らせ欄
JavaScript Primerの書籍版がAmazonで購入できます。
JavaScriptに関する最新情報は週一でJSer.infoを更新しています。
GitHub Sponsorsでの支援を募集しています。