本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。

最新情報

2018.09.18

ImageMagickを使うWebアプリのセキュリティ - 3. XSS・アクセス制御

本記事はImageMagick関連の記事の3本目です。ImageMagickの既知の脆弱性、システム情報の漏洩などの問題を扱った1つ目の記事、DoSを扱った2つ目の記事も参照ください。

最終となる3回目の今回は、XSSとアクセス制御を取り上げます。前提とする環境などは前回・前々回と同じです。

※ 記事中では右の略語を使っています。 IM = ImageMagick、CW = CarrierWave

アクセス制御の不備

初回の記事で述べたように、デフォルトの状態のCWは公開ディレクトリにアップロードされたファイルを置きます。ファイルの最終的な保存前に一時的に作成されるキャッシュも同様です。

これらのファイルはURLを推測できれば誰でも参照可能です。

【URLの例】
保存用 オリジナル http://host/uploads/book/picture/1/logo.gif
保存用 サムネイル http://host/uploads/book/picture/1/thumb_logo.gif
キャッシュ オリジナル http://host/uploads/tmp/1528992691-1593-0025-7417/logo.gif
キャッシュ サムネイル http://host/uploads/tmp/1528992691-1593-0025-7417/thumb_logo.gif

最終的な保存用画像のURLパスの構成は、既定値で下記のとおりです。

/uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}
         -> book                        -> picture    -> 1

キャッシュについては、その名前のイメージとは異なり、単に一時的に作成されるファイルです。最終的にユーザが登録を完了し保存用ファイルが作成された段階でキャッシュは削除されます。

キャッシュのURLを実際に目にするのは、確認画面を設けたWebアプリです。確認画面のHTMLソースに下記のようなタグが含まれているのが典型的です。

<!-- 確認画面のHTMLソース -->
<img src="/uploads/tmp/1528992691-1593-0025-7417/logo.gif" />
<input value="1528992691-1593-0025-7417/logo.gif" name="cache_name" type="hidden" />

キャッシュのディレクトリのランダムに見える部分(cache_id)は、下のソースのとおり、時刻、プロセスID、連番4桁、乱数4桁を連結した値です。「一瞬で推測可能」とまでは言えないものの、推測困難性を持たせる設計にはなっていません。

キャッシュのややこしいところは、ユーザが仕掛中のまま登録を完了しなかった場合だけでなく、画像の変換エラーや画像以外も含めたvalidationエラーなどが発生した場合にも、ファイルが削除されずに残ることです。エラー時にもキャッシュを必要とするWebアプリには便利ですが、これは後述する問題を引き起こすことがあります。

前段が長くなりましたが、本題のアクセス制御の問題について見ていきます。

割とよくあるアクセス制御の問題は、最終的な保存用ファイルにはアクセス制御があるのに、キャッシュには制御がないケースです。アクセス制御と言っているのは、

・キャッシュ画像のURL(上の確認画面の<img src=のURL)へのアクセスの制御
・確認画面のhiddenに入っているcache_nameを次の完了処理で使う際のアクセス制御

の両方です。これらが欠如していれば一般的には脆弱性とみなされると思います。

保存用ファイルとキャッシュの両方にアクセス制御がないケースもあり、悩ましいのはむしろこのケースかもしれません。なぜなら、最終版の保存用ファイルを全公開するのが要件であっても、確認画面の段階やエラー時に作られるキャッシュも同じだとは言い切れないからです。

例えば、もしユーザが秘密の画像を間違ってアップロードしてしまい確認画面で間違いに気付いて一安心したとしても、あるいは単にエラーが発生して途中で登録をやめたとしても、アップロードした画像がキャッシュとして公開ディレクトリに置かれているとしたら、単純に気持ちが悪いです。

次のXSSの話の中では、デフォルト状態のまま、つまりアップロードされたファイルがApache+Passengerの公開ディレクトリに置かれる前提で話を進めます。

XSS

下記は初回記事の図の再掲です。中身がJPEG、拡張子がpngのファイルをアップロードした際のサーバ側の処理を示したものです。

拡張子がpngのファイルをアップロードした際のサーバ側の処理画像

Apacheの公開ディレクトリ上にはオリジナル版とサムネイル版の2つが画像が置かれます。この両方がXSSの攻撃対象になりえます。

もし古いブラウザ(IE7)が相手ならばXSSは簡単です。図のようにコメント部分にHTMLとJavaScriptを入れたJPEGの拡張子をpngにしてアップロードし、オリジナル版にアクセスするだけです。

一方で現代のブラウザでのXSSにはちょっとした工夫が必要となります。ブラウザのContent-Typeの解釈は昔より厳密ですし、画像変換をパスするために妥当な形式の画像を与える必要があるからです。例えば、中身をHTMLにしたり、拡張子をhtmlにしたりしたファイルをアップロードしようとしても、画像変換のエラーによってアップロードはできず攻撃も失敗します(サーバにhtml2psがインストールされていない場合)。

ここで攻撃の手助けになるのは、IMが非常に多くの画像形式をサポートしていることです。その中には、Webサーバやブラウザにとっては完全に未知の形式も含まれています。

まずはオリジナル版、サムネイル版の両方を狙う一番単純な攻撃についてみてみます。

コメントを使ったXSS

IMはXV(Khoros Visualization image format, VIFF)という形式をサポートしています。コメント領域にscriptタグを入れた、中身も拡張子もXVの画像をアップロードしてみます。

中身も拡張子もXVの画像をアップロード画像

入力がIMにとって妥当な画像であるため、サムネイル作成のための画像変換は成功します。その結果、XV形式のオリジナル版とサムネイル版が「logo.xv」「thumb_logo.xv」というファイル名でApacheの公開ディレクトリに置かれます。

両画像へのHTTPリクエストの応答は下図のようになります。

両画像へのHTTPリクエストの応答結果

まず見てほしいのは下線のコメントです。デフォルトのIMは画像変換時にコメントを消さないため、オリジナル版だけでなくサムネイル版の方にもコメントが残ります。

上の応答でもう一つ重要なのはContent-Typeヘッダが存在しないことです。

Apacheは、拡張子マジックバイトによって、静的ファイルのContent-Typeを決定します。今回のようにいずれも未知の場合はContent-Typeを出力しません。

ブラウザにとってはContent-Typeがなく中身も未知の形式であるため、Edgeなどはこの画像をHTMLだと解釈してJavaScriptを実行します。つまり画像によるXSS攻撃が成功します。

さらに、「logo.html.xv」のような二重の拡張子を持つファイル名を使う攻撃方法もあります。Apacheは二重の拡張子を見てContent-Typeをtext/htmlにするため、全ブラウザでJavaScriptが動作します(二重の拡張子が使えるなら、単純に「logo.html.foobar」のような適当な拡張子を付けたJPEGを送ることでもXSSができる場合があります)。

ピクセルデータを使った攻撃

画像のコメントを使うXSSは、画像のコメントが削除される時や、画像出力時のContent-Typeがimage/*などに固定されている時には成功しません。

そういう時にも機能する可能性があるのは、画像のピクセルデータ(各ピクセルの色データ)やパレットを使う手法です。

筆者が使ったことがあるのはRGB形式の画像です。下図のように各ピクセルのRGBデータを単純に連ねた形式で、4ピクセル(2 x 2)の画像であればファイルサイズは12Byteになります。

12Byteの画像詳細

RGB画像にはマジックバイトやヘッダなどの固定部分がないため、先頭から末尾まで完全にファイルの中身を制御できます。一方で中身だけからは形式や縦横のサイズを判定できないため、RGB自体をアップロードしてもエラーになります。

ではこのRGBをどう攻撃に使うのか見てみましょう。いくつかの攻撃の可能性があると思いますが、ここではFlashを使う例を紹介します。

まずは、RGBへの変換後にSWFになるように、ピクセルデータをいじった画像を作成します。画像の形式は対象のWebアプリが受け入れるものならば何でもよいですが、ここではGIFとします(サムネイル作成時に拡大縮小されないサイズにしておきます)。このGIFの拡張子をrgbにして攻撃対象のサイトにアップロードすると、サーバ上での画像変換により、拡張子がrgbで中身がSWFのサムネイルが作成されます。

次に、攻撃者はこのSWFを読み込む罠のページを自身のサイト上に作ります。被害者が罠のページにアクセスすれば、被害者がアクセス可能な攻撃対象サイト上のコンテンツを窃取する攻撃が成功します(Cross-Site Content Hijacking)。Flashの場合、SWF出力時のContent-Typeによらずに攻撃が成功するというのがミソです。詳細はSoroush Dalili氏のブログを参照してください。

ところで、このRGBを使った攻撃が成功するのはRMagickが使われている場合だけです。MiniMagickは変換後の画像に対してidentifyコマンドを掛けて画像の情報を取得するようになっており、それに対応できないRGB形式の画像はその時点でエラーとなるからです。といっても、identifyに対応する形式に仕立てたPDFを使う攻撃などもありうるので、MiniMagickならば安全というわけではありません。

キャッシュファイルを使った攻撃

上の方で「HTMLの拡張子をhtmlにしてアップロードすると、画像変換でエラーになりアップロードは失敗する」と書きましたが、実はその表現は余り正確ではありません。

確かに画面上はエラーが表示され、オリジナル/サムネイル版とも作成されず、DBも更新されません。

アップロード失敗のエラー画像

しかし、アクセス制御の項で述べたように、実はエラーでも入力と同じ内容のキャッシュファイルがサーバ上に作成されることがあります。

キャッシュのURL例: http://host/uploads/tmp/1528994228-1593-0029-3952/logo.html

普通はエラーが起こると上図のような入力画面に戻ってしまうため、作成されたキャッシュのURLを知る術はありませんが、既に述べたようにこのURLの推測困難性は高くありません。総当たりでURLを突き止められれば、XSSやSWFによる情報窃取などの攻撃の可能性があります。

ただし厳密にいうと、エラーの際に必ずキャッシュが作成されるわけではありません。例えば、Uploaderクラス内で、オリジナル版に対するリサイズなどの画像処理の設定や、extension_whitelist(拡張子チェック)の設定をしており、それらでエラーになった場合にはキャッシュは作成されません。このような場合はキャッシュを使った攻撃は成立しません。

対策

上で説明したアクセス制御とXSSの問題に対する対策を書きます。前々回の記事も併せて参照してください。

アクセス制御の実装

先に述べたように、CWが作成するファイルには保存用とキャッシュの2種類があるので、これらを分けて考えたいと思います。

保存用

保存用については、画像にいつ誰のアクセスを許すかは業務要件です。全公開が要件ならばデフォルトのまま公開ディレクトリに置けばよいです。

アクセスを制限するのが要件ならば、画像を公開ディレクトリの外に置き、セッションをもとに権限チェックをするプログラムを通じて画像をブラウザに出力します。CWのwikiの説明どおり、保存用のファイルを置くディレクトリはstore_dirの設定などにより変更できます。

キャッシュ

キャッシュについてもアクセス制御は業務要件の範疇です。しかし、既に述べたように、仮に保存用を全公開にするとしても、キャッシュについては登録したユーザのみが参照できるよう一律でアクセス制御するのが自然だというのが筆者の考えです。

アクセス制御の具体的な方法は保存用と同じです。全公開するならばデフォルトのままとし、アクセス制御をするならばwikiの方法で公開ディレクトリの外にファイルを置きます。確認画面が無くアプリからキャッシュを明示的に参照しないならば、それ以上やることはありません。

アクセス制御が必要で、かつ確認画面を設ける場合は、もう少しやることがあります。

確認画面のHTMLソースをもう一度見てみましょう。

<!-- 確認画面のHTMLソース -->
<img src="/uploads/tmp/1528992691-1593-0025-7417/logo.gif" />
<input value="1528992691-1593-0025-7417/logo.gif" name="cache_name" type="hidden" />

ここで防ぎたいことは下記です。

①推測した他人のキャッシュ画像のURL(<img src=のURL)にアクセスして、
 - 他人のキャッシュ画像を盗み見る
②推測した他人のcache_name(hidden)を次の完了のリクエストに送ることで、
 - 他人のキャッシュ画像を盗み見る
 - 単純に他人のキャッシュを削除して妨害する

ログイン済みのユーザが使うフォームならば、下のようにセッションを使う対策が一番簡単でしょう。

1. 確認画面を表示する処理においてcache_nameをセッション変数に保存する。
2. セッション(1)をもとに権限をチェックしてキャッシュ画像を出力するプログラムを用意する。
3. 確認画面の<img src=には、プログラム(2)のURLを入れる。
4. 次の完了処理では、hiddenではなくセッション(1)のcache_name使用する。

目的は、1,2,3は①を防ぐこと、1,4は②を防ぐことです。

セッションを使わない場合は様々な対策が考えられます。詳細は割愛しますが、筆者がCWの枠組みの中で対策するならば、<img src=のURLにdata URIを使う(後述)、hiddenのcache_nameを推測困難な値にする、古いキャッシュをまめに消去する、というような対策を行うと思います。

画像表示時のXSS対策

下記は画像を出力するHTTPレスポンスの例(良い例)です。

HTTP/1.1 200 OK
Content-Type: image/gif          ← 正しいContent-Type (image/gif, jpeg, png)
X-Content-Type-Options: nosniff  ← MIME Sniffingを禁止
Content-Length: 1234

GIF89a...(GIF画像)               ← 正しいマジックバイトを持つ画像

基本的には、ブラウザに出力する画像の中身が妥当なJPEG, GIF, PNGのいずれかであり、Content-Typeヘッダがそれに応じたものであれば問題ありません。

その近道は前々回の記事の方法で画像の形式と拡張子などをチェックすることです。Apache上に静的ファイルとして画像を配置する場合、このチェックが済んでいるならば、あとはそのままファイルをディレクトリに置くだけでよいです。

さらに上のようにX-Content-Type-Optionsヘッダがあれば、ブラウザが画像をJavaScript, CSS, Flash, HTMLなどと誤って解釈する危険性を減らせます。

別のアプローチとしては、先にちらっと触れた、画像をdata URIでHTMLに埋め込む方法もあります。data URIは下のような形式のURIで、現行のメジャーブラウザはもれなく対応しています(caniuse)。

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUh...(Base64エンコードした画像)">

data URIの使用は性能に影響を与える可能性がありますが、XSS対策になるだけでなく、アクセス制御が簡単になる可能性があるのはメリットと言えます。特に一時的にしか使わないキャッシュ画像を確認画面に出力するような用途に向いていると思います。

まとめ

今回の記事では、アクセス制御の不備とXSSを取り上げました。

IMやCWに固有なところは、IMが多くの画像形式をサポートしていることを悪用したXSSくらいです。XSSについては、初回の記事で紹介した方法で画像形式を縛っておけば、あとは然るべきHTTPヘッダを付けて画像をブラウザに返すだけでよいです。アクセス制御について求められるのは、どの画像に誰がアクセスできるかを明確にして、それを実装するという一般的な対策です。

IM関連の3本の記事全体では、既知の脆弱性、システム情報の漏洩、DoS、アクセス制御の不備、XSSなどの脅威を取り上げました。対策としては、画像形式の絞り込み、サンドボックスの利用、ソフトウェのアップデート、リソース量の制限、アクセス制御の実装、画像表示時のXSS対策などです。

このように、セキュリティのために考慮すべきことは少なくありません。現状では残念ながら、画像のアップロード機能について「セキュリティを何も考慮しなくてもセキュアなものができあがる」ということはまずないのだと思います。

なお、他の一般的な画像アップロード関連の脅威としては、スクリプトのアップロードによるコマンド実行もありますが、Railsで見ることはほぼないため今回は取り上げていません。またセキュリティというよりはプライバシに関連する、画像の付加情報の扱いなどについても取り上げていません。



寺田 健 の他のブログ記事を読む

プロフェッショナルサービス事業部
寺田健