本サイトは、快適にご利用いただくためにクッキー(Cookie)を使用しております。
Cookieの使用に同意いただける場合は「同意する」ボタンを押してください。
なお本サイトのCookie使用については、「個人情報保護方針」をご覧ください。
Webサイトの診断でImageMagickを使ったアップロード画像の処理を見ることがあります。診断の結果、特有の興味深い挙動が見つかることもあるため、今回時間を取って検証してみました。
その結果を3回に分けて記事にします。1回目の本記事では、既知の脆弱性、システム情報の漏洩の問題を取り上げます。2回目はDoS、最後の3回目はアクセス制御やXSSを中心としたいわゆるWebアプリのセキュリティを取り上げます。
検証ではRailsとCarrierWaveを使ったWebアプリを使用しました(詳細は末尾)。CarrierWaveはRailsなどで古くから使われているファイルアップロード処理用のgemライブラリです。これらを選んだのは、筆者の経験上、ImageMagickとともに使われていることが多く、また挙動が特徴的だからです。
以下では、ImageMagickの概要と、CarrierWaveの基本的な挙動について説明し、その後に脆弱性の概要と対策を説明します。
※ 記事中では右の略語を使っています。 IM = ImageMagick、CW = CarrierWave
8/22にIMにも影響するGhostscriptの脆弱性(VU#332928)の注意喚起がJPCERTより出され、前後して関連する情報(Cybozuラボブログの情報、yoya氏やozuma氏の情報など)が出ています。それらも確認することをおすすめします(本記事は内容的にこれらの情報とかぶっている部分があります)。
ImageMagickの概要
ImageMagick(IM)の特徴の中で本記事のテーマに関連しそうなものを3つ挙げます。
対応する画像形式が多い
IMは200種類以上の画像形式に対応しています。PHP-GDやJavaのImageIOはせいぜい10種類なので段違いの多さです。
連携するソフトウェアが多い
IMが200種類全てを自前で処理するわけではありません。多数の共有ライブラリを呼び出したり、delegateという仕組みにより他の様々なツールのコマンドを起動することによって、多くの画像を処理しています。
極めて大きな画像も処理可能
IMはTP(テラピクセル)サイズの画像に対応しています。PHP-GDやImageIOは、総ピクセル数が31bit、つまり約2GP(ギガピクセル)を超える画像や、メモリに収まらない画像などが処理できません。
記事の中で出てくるので、ここでIMの使い方についても書いておきます。
IMはプログラムからもコマンドラインからも使えます(GUIはありません)。コマンドラインでは、identify, convert, mogrifyの3つが代表的なコマンドです。identifyの主な用途は画像のサイズや形式などの情報取得、後の2つは画像の変換/加工です。
RubyからIMを使うには、RMagickやMiniMagickなどのgemライブラリをインクルードします。RMagickはIMの共有ライブラリを通じて、MiniMagickはIMのコマンド(identify, mogrify)を通じて、IMの機能を利用します。現在メンテされているIMのバージョンは6系と7系の2つですが、7系に対応しているのはMiniMagickだけです。
CarrierWaveの挙動の概要
CarrierWave(CW)はファイルアップロード処理用のgemライブラリです。画像に特化したものではありませんが、RMagickかMiniMagickを使い、簡単にサムネイルなどを作れるようになっています。
完全に素の状態のCWはIMを使った画像処理をしないので、ここでは下記のサムネイル版を作成する設定だけをしているとします。
class PictureUploader < CarrierWave::Uploader::Base
include CarrierWave::RMagick
# サムネイルを作成する設定
version :thumb do
process :resize_to_limit => [100, 100]
end
この時のCWは少し奇妙な挙動を見せます。例として、JPEGの画像を拡張子pngでアップロードした際の処理を説明したものが下の図です。
画像をアップロードすると、オリジナル版とサムネイル版の2つのファイルがサーバに作成されます。
オリジナル版の中身やファイル名はリクエストボディーのものと同じです。つまり画像の中身(JPEG)と拡張子(png)が一致しない状態となります。
興味深いのはサムネイル版の方で、こちらは中身がJPEG→PNGに変換されます。これは、IMがファイル名の拡張子であるPNGを出力形式として画像を変換するためです。
もう少し詳細を書くと、IMは画像の中身と拡張子から入力の形式を推定します。中身が優先なので、この例では入力の形式はJPEGだと判定します。変換先の形式は拡張子から決定するので、この例ではPNGです。ただし不明な拡張子の場合は入力と同じ形式を変換先とします。もちろん画像変換はJPEGやPNGに限って行なわれるわけではなく、中身をSVGにして拡張子をtiffにすればSVG→TIFFの変換がされますし、中身も拡張子もGIFならばGIF→GIFの変換がされます。
サーバ側の画像ファイル(オリジナル版、サムネイル版)は公開ディレクトリに置かれます。そのファイル名はアップロードのリクエストで指定されたファイル名と基本的に同じです(厳密には、URLデコードされたり、非英数文字の多くがアンダースコアに置換されたりします)。
上記をまとめます。
・サーバ側でIMを使ったアップロード画像の形式変換が行われる。
・変換元(入力):入力画像の形式は画像の内容などから自動判定される。
・変換先(出力):変換先の画像の形式はファイル名の拡張子などにより決定される。
・入力/出力とも様々な画像形式に対応している。
・変換後の画像は公開ディレクトリに置かれる。
ポイントとなるのは、特に制限を施していないCWでは「入力と出力の画像形式を攻撃者がほぼ自由に選べる」という点です。これは、今回・次回以降の記事で見るように、攻撃側に様々な選択肢を提供します。
前段の概要説明は以上です。次から脆弱性の話に入ります。
既知の脆弱性
ImageTragickの年(2016年)以降のIMの脆弱性報告はかなりの数にのぼります。CVE Detailsによると、2017年は357個、2018年は執筆時点で32個ものCVEが公開されています(この数字は公開年が基準です。CVE番号の年でいうと2017年は236個、2018年は22個です)。
IMのCVE数(CVE Details掲載の表から抜粋 2018年6月頃)
2017年の357個は、単純計算でほぼ毎日CVEが振られていることになります。OSなどの大規模なソフトウェアではありうる数字ですが、それ以外のソフトウェアとしてはかなり多いと言えます。
内訳をみると、脆弱性の種別としてはDoSが多いものの、OverflowやCode Executionも含まれています(ImageTragickほど攻撃が容易なものでは無さそうではありますが)。
影響するIMのバージョンについては、最近の報告の8割くらいが7系のみを挙げていますが、それらの中にも実際に試してみると6系に影響するものがあり、6系の方がセキュアだと考えない方が安全だと思います。
脆弱性に関連する画像形式を、2017年のCVEの新しい順に何件か見てみると、実に様々です。
MSL (CVE-2017-17934) PNG (CVE-2017-17914) MNG (CVE-2017-17887) PSD (CVE-2017-17886) PICT (CVE-2017-17885) |
PNG (CVE-2017-17884) PGX (CVE-2017-17883) XPM (CVE-2017-17882) MAT (CVE-2017-17881) WEBP (CVE-2017-17880) |
MNG (CVE-2017-17879) WPG (CVE-2017-17682) PSD (CVE-2017-17681) XPM (CVE-2017-17680) PNG (CVE-2017-17504) |
IMに脆弱性が多い理由はいくつかあるのでしょうけども、対応する画像形式の多さも一因であることはこのリストから見て取れると思います。
ユーザの視点でこのリストを見ると、処理する画像の形式を少数に絞れば、自身のシステムに影響する脆弱性を減らせることが分かります。逆に、形式を絞っていないのであれば、(ImageTragickの時と同様に)受け付けるつもりがない形式の画像をアップロードされて、IM自体や連携する他のソフトウェアの脆弱性を突かれてしまうかもしれません。
筆者の経験でも、画面には「JPEG, GIF, PNGのみアップロード可」と書いてあるものの、システム的には画像形式の縛りがされていないサイトを見ることがあります(CWにおいてextension_whitelistにより拡張子だけを絞っている場合、入力画像の形式の縛りがない状態になります)。実際に、他の形式の画像をアップロードすることで、IMや連携するソフトウェアの既知の脆弱性を悪用できることもあります。
システム情報の漏洩
IMでの画像変換の際に、サーバ上のファイルの絶対パスなどが画像に埋め込まれる場合があります。
例として、中身はGIFで拡張子をinfoとした画像を検証環境にアップロードしてみます。下記はIMが画像変換で生成したINFO形式のサムネイルです。
INFOは画像のサマリ情報をテキストにした形式です。サーバ上の一時ファイルの絶対パス(Railsのプロジェクトディレクトリを含む)が先頭に含まれています。
もう一つ例を見てみましょう。下記は拡張子をeps3にした際のサムネイルです。
同様にパスが含まれています。その上の行にはIMのバージョンも出力されています。
このような現象はinfoやeps3だけで発生するわけではありません。筆者の経験では、拡張子をhやshtmlにした際にファイルパスが画像に出力された例があります。ネット上の情報で言えば、PNGでのパスの埋め込みを指摘したMediaWikiのバグ報告があります。
ただし、パス埋め込みの挙動は環境により若干の差があります。環境というのは、IMのコマンドを実行する際に付けるオプション、使用するライブラリ(RMagick or MiniMagick)、IMのバージョンです。上記はCW, RMagick, IM 6.7.8(CentOS)での例です。
この挙動の原因はIM側の配慮不足にあるとも言えますが、INFOなどの形式の画像にパスが含まれるのはある意味当然の仕様だとも言えます。筆者が記事の末尾に記した3つのバージョンのIMで確認したところ、JPEG, GIF, PNGの3形式間の変換では(余分なコマンドオプションを付けない限り)パスの埋め込みは起こらないため、画像の形式を制限して対策するのがよいでしょう。
対策
上で説明した脆弱性に対する対策を見ていきます。前提としてJPEG, GIF, PNGのアップロードだけを許可することとします。またIM7+MiniMagickの使用を想定しています。
画像形式の絞り込み
画像形式を絞る目的は、IMや連携するソフトウェアの既知/未知の脆弱性を突いた攻撃や、次回以降に説明するDoSやXSSのリスクの低減です。副次的な効果としてサーバの内部パスの漏洩も防げます。
具体的な方法としては、IMのセキュリティポリシーを使う方法と、プログラムによる方法の2つがあります。それぞれ以下に説明します。
ImageMagickのポリシーによる絞り込み
入出力両方の形式を緩く絞る方法として使えるのが、IMのセキュリティポリシーです。
下記はその設定の例です。
<policy domain="delegate" rights="none" pattern="*" />
<policy domain="filter" rights="none" pattern="*" />
<policy domain="coder" rights="none" pattern="*" />
<policy domain="coder" rights="read | write" pattern="{JPEG,GIF,PNG}" />
この例では、全てのdelegateとfilterを無効にし、coder(画像のエンコード/デコードを行うモジュールのようなもの)をJPEG, GIF, PNGの3種に制限しています。つまり、SVGや、PDFや、PostScriptや、MPEGや、その他の諸々の形式を入力/出力として受け入れることはなくなります。
ポリシーを記述するのは、/etc/ImageMagick* などの下にあるpolicy.xmlファイルです。このファイルの設定は、他のソフトウェアやコマンドラインからIMを使う場合にも適用されます。Webアプリだけに独自のポリシーを適用したい時は、IMの環境変数が使用できます。
ところで、このようにホワイトリスト的に一部のcoderのみを有効にするポリシー設定は、ImageTragickの頃には無かったものです。IMの本家やawm-Techブログによると、7系は7.0.4-7から、6系は6.9.7-7から利用できます。いずれも2017年にリリースされたバージョンです。
注意したいのは、ポリシーによる画像形式の絞りには「緩さ」がある点です。例えば、PNGのcoderが有効であれば、MNG(Multiple-image Network Graphics)の画像も許可されてしまいます。これは、ポリシーで絞っているのは画像形式ではなくcoderであり、coderには亜種的な画像形式も紐づいているからです。
したがって、画像形式に着目した、次項のプログラムによるチェックもやっておくのがよいです。このチェックには、IMの脆弱性対策に加えて、次回以降に説明するXSS対策の意味があります。
プログラムによる絞り込み
下記はそのチェックをするプログラムの例です。画像の入力形式の絞りとしてマジックバイトを、出力形式の絞りとして拡張子をチェックしています(拡張子により変換先の画像形式が決まるため)。ついでに、XSS対策を意図して拡張子と中身の整合性もチェックしています。
# マジックバイト, 拡張子それぞれのチェック & 両者の整合チェック
# ActionDispatch::Http::UploadedFileを引数に取る
def check_image(file)
ext = File.extname(file.original_filename).downcase # 拡張子
data = file.read(20) # 中身の先頭
return ext == '.gif' && data.match(/\AGIF8[79]a/) \\n || ext == '.png' && data.match(/\A\x89PNG\r\n\x1A\n/n) \n || ['.jpg','.jpeg'].include?(ext) && data.match(/\A\xFF\xD8\xFF/n)\
end
目的の一つはIMの脆弱性対策ですので、IMに画像が渡る前のタイミングでこのプログラムを実行させて画像形式を絞ってしまいたいです。それを確実にするため、筆者の検証アプリでは、ModelやUploaderクラスではなく、Controller内でこのメソッドを呼んでいます。
今回プログラムの自作に至ったのは、CWの設定や既製のgemライブラリなどを調べたものの、チェックをIMに渡す前のタイミングで行ない、かつ拡張子と中身の整合チェックもするうまい方法を見つけられなかったためです。そういうライブラリが無いせいか、実際の診断の中でも、中身のチェック、もしくは拡張子と中身の整合チェックが漏れているケースはよく見られます。
画像形式の絞り込みという点では、上記のポリシーよりもこのプログラムの方が厳密なので、ポリシーの設定は不要だと考える方もいるかもしれませんが、筆者は両者を併用するのがよいと考えています。理由は、両者の画像形式の判定方法は微妙に違うかもしれませんし、ポリシーの方には「IMの大元でモジュールのレベルで縛る」という確実性があるためです。
サンドボックスの利用
IMの脆弱性に対する保険的な対策として挙げられるのがサンドボックスです。
今回紹介するのはGoogleが公開しているnsjailというツールです。これを使うと、ファイルアクセス、ネットワークアクセス、システムコール、リソース量などを制限した、仮想的な隔離環境でIMを実行できます。
一般にこの手のツールは導入の手間がかかることが難点ですが、nsjailにはIM用の設定のひな型が付属しており、またnsjailをPaperclipに適用した際の手順を記したPatrick Figel氏のブログという詳細な導入ガイドがあるため、導入の敷居は比較的低いです。
ただし、nsjailは新しいkernelでの動作を前提としており、本記事の主な対象であるCentOSでは完全には動作しません。Debian系の環境では動作するので、それらのユーザ向けに以下説明します。詳細はFigel氏の記事に書かれているので、差分だけをかいつまんで説明します。
まずは設定ファイルの作成です。MiniMagickは内部的にidentify, mogrifyコマンドを起動するので、両コマンド用の設定ファイルをそれぞれ作成します。identify用の設定はFigel氏の設定が、mogrify用の設定はnsjailに付属のconvert用の設定のひな型がベースになります。
設定ファイルの変更は、mountやsyscallの追加などわずかです(設定ファイルの完全版は筆者個人のGistにあります。使用する際には、動作に支障がないかを自身で確認してください)。
### identify用の変更 ###
# Mount追加 (Railsのプロジェクトディレクトリ)/tmp
mount {
src: "/var/www/rails_projects/first_books_app/tmp"
dst: "/var/www/rails_projects/first_books_app/tmp"
is_bind: true
}
# Syscall変更 execve → execveat
seccomp_string: "POLICY imagemagick_identify {"
seccomp_string: " ALLOW {"
...
seccomp_string: " execveat, getdents, getcwd, readlink, getrlimit,"
### mogrify用の変更 ###
# Syscall追加 renameだけ追加
seccomp_string: "POLICY imagemagick_mogrify {"
seccomp_string: " ALLOW {"
seccomp_string: " rename, read, write, open, close, newstat, newfstat,"
次は、nsjail経由でIMのコマンドを起動するためのshellスクリプトを適当な場所に置きます。中身はFigel氏のものとほぼ同じです。そして、本物のコマンドの代わりにこれらを起動するようにMiniMagickの設定を変更します。
# shellスクリプトを置いたディレクトリをcli_pathに設定する
MiniMagick.cli_path = "/opt/jailed-im"
これで、Webアプリ経由でIMを起動すると、サンドボックス内でIMが動作します。
実際に試してみると、許可していないファイルへのアクセスやシステムコールの呼び出しはエラーとなり、IMや関連するソフトウェアの脆弱性対策として効果が期待できます(次回説明するDoSの対策としても便利に使えます)。
利用に際してのデメリットは、Figel氏の記事のとおり、性能面でのペナルティがあることです。また、初期導入時の手間に加えて、IMなどのアップデートの際にも設定変更が必要となる可能性があります。
ソフトウェアのアップデート
上記の画像形式の絞り込みにより防げるIMの脆弱性もあると思いますが、許容する形式の処理に脆弱性がある場合などはどうにもなりません(実際にPNGなどのcoderの脆弱性も見つかっています)。既知の脆弱性については、脆弱性の情報を収集し、タイムリーにソフトウェアをアップデートすることが対策の基本です。
他のソフトウェアと同じく、IMの脆弱性情報はNVD, JVNなどから収集できます。アップデートは、Github上のソースコード(7系, 6系)か、RPM(IM本家, Remi)から行えます。CentOS7のRPMは2016年の6.7.8.9-15以降は更新がありませんが(執筆時)、本家やRemiのRPMは7系を含む新しいバージョンです。
IM本家のリリースサイクルは短く、月に数回のリリースがあります。
まとめ
近年IMには多くの脆弱性が発見されています。こまめに脆弱性情報を確認してアップデートすることをお勧めします。
デフォルト状態のCWとの組合せでは、入力・出力の形式にほぼ制限がない画像変換が可能です。IMがほぼ丸裸でさらされている状態と言えるでしょう。この状態では、既知の脆弱性を突く攻撃や、システム情報の漏洩や、次回以降に取り上げるXSSやDoSなどにおいて、攻撃側が多くのオプションを持つことになります。
既知の脆弱性について言うと、IM自体の脆弱性に加えて、IMが入り口となり連携している様々なソフトウェアの脆弱性を突く攻撃の可能性があります。許容する画像形式の種類が多いと、そういったリスクもまた大きくなります。
IM自体もホワイトリストでcoderを絞れるようにするなどの改善を行っています。そういった機能を(足りないところは補いながら)活用してセキュリティ対策を行なえば、上に挙げたような様々な攻撃のリスクを減らせます。さらに、導入や保守の手間は増えるものの、サンドボックスを利用すればIMの脆弱性の悪用は一層困難になります。
若干話がそれますが、IM以外の画像処理ツールについて触れておきます。これまで、画像のアップロードを扱うgemライブラリの多くはIMのみに対応していましたが、比較的新しいShrineはlibvipsというツールにも対応しています。筆者自身は試しておらず確かなことは書けませんが、最近標準となったActive Storageでもlibvipsがサポートされたため、今後IMに代わる選択肢として利用が増えていくかもしれません。
本記事ではCW+IMの環境を前提としていますが、内容の一部は他の環境にも適用できると思います。他の環境でIMを使用している方にも参考になれば幸いです。
検証環境
今回の検証で使用したソフトウェアのバージョンは下表の通りです。脆弱性の検証は主にIM 6.7.8-9の環境で実施しました。これはCentosOSのレポジトリの検証時の最新版です。IM 7.0.7-38(CentOS IM本家のRPM)、6.8.9-9(Ubuntu deb)も同じです(検証 2018年6月頃)。
OS | Centos 7.5 (3.10.0-862.el7.x86_64 #1) Ubuntu 16.04.4 (4.4.0-116-generic #140-Ubuntu) |
---|---|
ImageMagick | 6.7.8-9 (CentOS ImageMagick-6.7.8.9-15.el7_2.x86_64) 7.0.7-38 (CentOS ImageMagick-7.0.7-38.x86_64) 6.8.9-9 (Ubuntu 6.8.9.9-7ubuntu5.11) |
Apache | 2.4.6 (CentOS), 2.4.18-2 (Ubuntu) |
Ruby | 2.3.6p384 (CentOS), 2.5.0p0 (Ubuntu) |
Passenger | 5.3.1 |
Rails | 5.2.0 |
CarrierWave | 1.2.2 |
RMagick | 2.16.0 |
MiniMagick | 4.8.0 |
nsjail | 2.3 |
検証用のRailsアプリは、Railsの教科書などを参考にベースを作りました。
おすすめ記事