ドラフト。ドラフトじゃなくなることはあるんだろうか。コメント求む。
XSS対策手法のひとつの提案
概要
XSS (cross site scripting)というのは、特にwebプログラミングの文脈でよくつかわれる、セキュリティホールの一種である。webプログラミングが一般的になった現在でもこのセキュリティホールは頻繁に発生しており、必ずしも素人だけがしてしまう失敗とも限らない。近年で有名な例としてtwitterでの事件がある。
>本投稿では、XSSの事例と背景について説明するとともに、その対策として「コンテキストを減らす」「コンテキストごとのエスケープ」「安全なプログラミングの手法」を示しひとつのXSS対策の指針を示す。
また最後に、もともこもない対策方法として「base64で対策したらこうなった」という話をする。
本投稿は、「安全な」対策方法としてはまだ開発段階にあり、鵜呑みにするのは危険であることをご了承いただきたい。自己責任。
目次
XSSとは
XSSとはCross Site Scriptingの略であり、掲示板などのユーザ投稿型のwebサービスに対して「悪意のあるスクリプト」を投稿し、開発者の意図しない動作をさせるセキュリティーホールの一種である。
Smartyテンプレートとphpでの具体的な例を挙げる。以下の様なtest.phpとtest.tplがあったとする。ここでは、簡単のため必要なrequireや定数・変数の定義などは省いている。
この例では、例えばform1に次の値を渡すとjavascriptが実行される。
本来であれば、適切に文字列が処理されform1の文字列は、文字としてブラウザに表示されなければならない。しかし、適切な文字列処理が行われていないためブラウザ上では下記の様なHTMLとして解釈され、開発者の意図してないjavascriptが実行されてしまう。
このように、適切な文字列処理が行われないまま出力された値をブラウザが解釈することで、開発者の意図しない動作を発生させる脆弱性のことをXSSと呼ぶ。上記はその一例である。
なお、本投稿では筆者の主たる開発環境のなかで一番一般的であると考えられるPHPとsmartyテンプレートで考えるが、全ての言語・テンプレートで共通のことがいえる。
XSSの原理と背景: コンテキストとエスケープの関係
XSSを防ぐ一番一般的な方法はエスケープという文字列処理である。例えば先ほどの例では、以下の様なエスケープが行われる。
前:<script>alert("XSS!");</script>
↓
後:<script>alert("XSS!");</script>
ここでは、HTMLのメタ文字である「<」や「>」を「<」「>」に変換している。ここでいうメタ文字とはあるプログラムの文字列中で特殊な意味を持つ文字のことである。<や>はHTMLではタグを示す特殊な文字であり、これらはHTMLのメタ文字である。これらを文字列として解釈させるための変換をエスケープと呼ぶ。
また、ここでコンテキストという概念を導入する。コンテキストとは、プログラム中の注目している領域の文字が何であるかを示すものである。例えばHTMLでは、以下の様なコンテキストが存在する。
他にも、様々なコンテキストが存在する。
XSSの原因は、コンテキストに対して適切なエスケープがされていないことにある。先ほどの例では、「HTMLの文字列」のコンテキストでHTMLのメタ文字をエスケープ出来ていなかったためXSSが発生している。HTMLのメタ文字である「<>」をエスケープしHTMLの文字列のコンテキストに合わせなくてはいけない。
つまり、ひとつのコンテキストの中にメタ文字を含めるとXSSが起き、メタ文字をエスケープして文字列の中をひとつのコンテキストにすることでXSSが防げる。
XSS対策が困難な理由と、その対策
XSS対策を困難にしている理由として、筆者は「コンテキストの多さ」と「コンテキストごとのエスケープが提案されていない」ことに注目する。
コンテキストは先ほどの単純なHTMLの例でも多数存在することがわかる。筆者が思いつく限りでも以下の様なものがある。
このコンテキストごとにメタ文字が存在し、それぞれに対して適切なエスケープ処理を行う必要がある。
しかし、PHPやsmartyでは十分なエスケープ関数が提供されていない。むしろ、「HTMLの文字列」に対するエスケープ関数しか無いといえる。そのため、エスケープ関数を通しているにもかかわらずXSSが起こる。
例えば以下の例がある。引用。
上記の様な例では、$fugaに代入されている文字列の末尾にバッククオートを挿入することで、XSSを行うことができる。Smartyにはjavascriptの文字列のコンテキストでのエスケープ処理が存在するが、この例では適切なエスケープ関数を選択しないことでXSSが起きることを示した。
このように、コンテキストが多数存在するため初心者にはそれが判断しにくくなっている。また必ずしも適切なエスケープ関数が用意されているとも限らないため適当なエスケープ関数を選んだり、適当に組み合わせたりする開発者もいるようである。
このような状況がXSSの対応を難しくしていると考えられる。
そこで、筆者は「コンテキストの削減」と「コンテキストごとのエスケープ」を提案する。
コンテキストの削減
XSSを議論するとき、エスケープばかりに目がいき勝ちであるが、今までの議論で述べた通りコンテキストの多さが混乱を招いているという一面がある。そこで、この混乱を回避するために、以下のコンテキストに絞り込むことを検討する。今回は特にPHP、smartyテンプレートでHTMLを出力することを考える。
(ここでこれに絞り込む根拠を書きたい。一言でいうと、これ以外は大体プログラミングの努力でどうにか出来る。)
ここで以下の様な場合に注意したい。
この例は、HTMLとして出力される予定の部分にsmartyで値を埋め込む場合である。これは文字列中にのみ注目した場合「HTML」のコンテキストと「HTMLの文字列」のコンテキストが混在してしまっている。つまりこれは単純な「javascript中の文字列」のコンテキストではなく、新たなコンテキストである。
この例では、dom操作で同等の処理ができるためコンテキストを増やすこと無く対処できる。
この例の様に一見して同じコンテキストに見えるものでも、実際は異なる(二種類以上のコンテキストを混ぜている)場合があるため注意が必要である。そしてここが非常に難しいため文字列連結などには注意が必要である。
また、「javascriptでのdocument.writeでの出力」や「innerHTML」などのコンテキストの必要を主張する読者もいるかもしれないが、多くの場合DOM操作での代替が可能でありdocument.writeが必要な場面は限られる。従って、これらは使うべきではないし、使う場合は知識豊富なプログラマが行うべきである。
コンテキストごとのエスケープ
上記で定めたコンテキストごとのエスケープ処理について議論する。筆者は上記のコンテキストごとにひとつのエスケープ関数を用意している。先に議論した通り、コンテキストごとに必要なエスケープ関数はひとつである。この例の様にhtml,jsなどの複数のエスケープ関数をカスケードして処理することも不可能ではないが、次の理由から筆者はおすすめしない。
もしjsの関数にバグがあった場合、jsを利用している全箇所に影響が出る。js関数が複数のコンテキストで使用されていた場合、かならずしも複数のコンテキストで正しく処理できることを保証することはできないため、全箇所で動作確認をし直す必要がある。したがって、エスケープ関数はひとつのコンテキストごとにひとつ用意すべきである。
それぞれの具体的な実装について議論したい。
(したいといいつつ、自分の関数さらしていいいものか判断しづらいので、今度書く)
安全なプログラミングの指針
ここでは、XSSに対して安全でシステムを壊しにくいプログラミング手法について議論する。
筆者がおすすめする方法は以下である。
データを生の状態で変数に格納する理由は、先の説で述べたのと同様に、コンテキストごとの適切なエスケープが困難になるからである。これについては次節で例をあげて説明する。
出力時には、必ず適切なコンテキストのエスケープをひとつだけ行う。複数のエスケープを行うと問題が起こる可能生を前説で説明した。エスケープを忘れることは論外であるが、これについてはコミット時やデプロイ時にフックでチェックする方法が多く存在する。またsmartyを改変しエスケープ関数がついていないとエラーを出し知らせることも行われている。
dom操作以外のjavascriptからの出力をしないことについても前説で議論した。多くの場合、javascriptからの出力はdom操作に代替可能であり、textnodeを作りテキストで出力することでXSSを防げる。
すべてをhtmlエスケープしてしまうfail safeの欠点
これはここで提案されている方法である。以下に引用する。
ちなみに自分は、文字列テンプレート+innerHTMLというのを好んで使うので、自社サービスに付いてはサーバー側であらかじめエスケープされているJSONを使うのを好む。これはJavaScriptで余計な文字列処理をしなくていいというメリットがある。
簡単にいうと、javascriptに値を渡した段階でhtmlエスケープされている状態にしておき、javascriptで全くエスケープ処理をしなくても大丈夫な様にする処置である。つまり何も考えずにdocument.writeを実行しても安全である。
この方式の欠点は、document.writeをする場合以外ではhtmlエスケープを元に戻す必要がある点である。例えば、ajaxに値を渡す場合やjavascript上で文字列処理をする場合などである(後者はあまりないかもしれないが)。php上でのエスケープ処理を把握し、逆エスケープ処理をjavascript上で構築する必要があり保守性の低下やバグの温床になりやすい。もちろん、サイト構築時のポリシーや設計次第では有効であるが、汎用的な解決策であるとはいい難い。また、すでに構築済みのサイトである場合ほかのエスケープ処理とバッティングしやすく、導入が困難である場合が多い。また、この方法で設計したサイトを後に変更することも同等に困難であり、設計の柔軟性が損なわれる。
先に議論した通り、多くの出力操作はdom操作に代替可能であり、差し迫った必要がない限りはそもそもエスケープする必要がない。
この手法は初期開発時の設計次第では大変有効ではあるが、dom操作の有用性や既存のシステムへの導入の困難性、システム変更の困難性から、あまりおすすめできないと筆者は考える。
もともこもない対策: base64
これまで、コンテキストとエスケープの関係を議論してきた。XSS対策の困難さはコンテキストにあったエスケープ手法を選択できないところにあることを示した。
ここで、筆者が最近諸事情でとった手法がbase64でエンコードしてしまう手法である。実はbase64で値を埋め込みjavascriptやphpのそれぞれでデコードすれば多くの場合は安全になるのである。もう一度XSSの起きやすいコンテキストについて考えてみよう。
多くの場合がこれの類型であり、ダブルクオーテーションで囲まれている範囲内に特定のメタ文字を挿入することでXSSを起こすようになっている。一方でbase64の文字種を見てみよう。
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=
htmlやjavascriptで危険な文字種が一切現れない。これを受け取った側でdecodeすれば、XSSの危険性を減らしながら生データを扱うことができることがわかる。URLセーフでない文字列も存在するが、これには代替があるためURLセーフにすることも可能である。
つまり、コンテキストがわからないならbase64エンコードしてしまえば、少なくとも値の受け渡しで失敗することは激減する。javascriptの出力側でdocument.writeや値の連結について注意すればXSSの問題はほぼ起きないと筆者は考える。
base64は、その変換ルールからデータのサイズは必ず4/3倍になることが知られており、データサイズの管理も簡単である。また、変換ルールは単純であるため効率よく変換もできる。
base64を使えば100%安全ということはあり得ないが、細かな詳細な議論ができない環境では有効である。
まとめ
本投稿では、XSSの対策や包括的理解をすすめることを目指し、その対策に対する考え方や施策のひとつを提案しました。
まず、XSS一般についてふれ、その原因や背景について述べました。コンテキストという概念を示し、それとエスケープ処理の対応について述べました。そして、コンテキストを絞り込みそれぞれに対してひとつずつエスケープ方法を用意するというシンプルな考え方を提案し、それがXSSに対応できることを説明しました。
個人的には「なんで今さらXSSなの?」といわれそうだなとも思ったんですが、直近でこのことで大変もめた経緯があり、また、結局包括的な説明などがあまりない状況をどうにかした方がいいだろうなと思いブログ記事にしました。
長文過ぎるので、「単純なハウツー」を求めている人には役立たないかもしれないですが、みなさまの勉強の役にすこしでも立てればなと思います。
2 Responses for "XSS対策手法のひとつの提案"
[...] This post was mentioned on Twitter by わかめにゅーす(PHP), hrjn2. hrjn2 said: ブログ書いた: XSS対策手法のひとつの提案 http://blog.gijutsuya.jp/harajune/2010/11/19/one-method-for-xss-protection/ [...]
[...] XSS対策手法のひとつの提案 – 進・日進月歩 (tags: xss) [...]