ImageMagick

「さようなら ImageMagick」の考察

はじめに

サイボウズさんの ImageMagick の利用をやめる記事について少し思う所を書きます。否定でなくアシストのつもりです。

自分のスタンスを3行でまとめると、

  • policy.xml で読み書き出来るファイル形式を絞れば、いうほど怖くはない
  • ただ、ImageMagick に限らずサーバサイドで動かすのは手間と覚悟が要る
  • Yahoobleed の件でコード品質が信用ならないと言われたら、ごめんなさい

「ImageMagick を外した理由」

サイボウズさんのブログでは、2017年の ImageMagick 脆弱性報告数が多いので駄目との事です。

脆弱性

ImageMagick には脆弱性が大量に存在します。
2017 年に報告された ImageMagick の脆弱性は 236 件 でした。
大量にある上にリモートコード実行級の脆弱性もあり、
安全性という観点ではかなり厳しい評価をしなければなりません。

脆弱性は数だけでなく、その中身も大事です。
何もしない場合は元記事の通りですが、脆弱性に傾向があり対策がある程度できます。

スクリーンショット 2018-08-26 2.03.55.png

ざっとリストを見ると、ReadDIBImage、WriteBMPImage といった画像ファイル形式の読み書きの不具合が多い事に気づくでしょう。画像ファイルのデコーダーがシステムの前面に立つといった事情もあります。

% ls git/ImageMagick/ImageMagick/coders/*.c | wc
     128     128    6880

ImageMagick は組み込まれたコーデックで 100 種以上、Delegateの外部プログラム連携を含めると200種以上の膨大なファイル形式に対応していて、古式ゆかしい Sun 形式から最近では WebP, HEIC(HEIF) 形式まで扱えます。読み書き出来る画像ファイル形式の数だけ脆弱性が多く出やすいのです。

これらの状況は、ImageMagick でユーザから任意の画像ファイル形式を受け取って処理するのは自殺行為である事を示唆します。
逆にいえば、読み取る画像ファイル形式を最小限に絞れば安全とまでは言いませんがかなり安全に近づきます

  • policy.xml で JPEG/GIF/PNG/WebP のみ許可する設定にする
  • 画像ファイルのバイナリ先頭を見て JPEG/GIF/PNG/WebP のシグネチャ以外は弾く
    • なお、ブラックリスト方式で自力チェックする場合は、GZIP がすり抜ける罠にご注意を。convert in.png.gz out.png みたいな事出来るんで。
  • 脆弱性をつかれても被害をある程度限定できる VM 環境で動かす

そこそこの規模のサイトで ImageMagick を使う場合、これらの組み合わせで対応をしているでしょう。(やってなかったら、今すぐして下さい!)

尚、policy.xml での設定は以前はかなり面倒でしたが、6.9.7-7で改善されました。 (2017年2月初頭リリース)

典型的には /etc/ImageMagick-[67]/policy.xml に以下の4行を入れるだけです。必要に応じてファイル形式を増減して下さい。

<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="{PNG,JPEG,GIF,WEBP}" />

-list policy で確認できます。

% convert -list policy

Path: /home/yoya/ImageMagick/master/etc/ImageMagick-7/policy.xml
  Policy: Delegate
    rights: None
    pattern: *
  Policy: Filter
    rights: None
    pattern: *
  Policy: Coder
    rights: None
    pattern: *
  Policy: Coder
    rights: Read Write
    pattern: {GIF,JPEG,PNG,WEBP}

Path: [built-in]
  Policy: Undefined
    rights: None

是非、ご活用を。

さて、最低限するべき対策の話はここまでにして、近年出してしまった ImageMagick脆弱性クリティカルヒットを2つ紹介します。

クリティカルヒット(1) ImageTragick (CVE-2016–3714)

ImageMagick が対応する画像ファイル形式の中にコマンドを含められるものがあって、何でも実行出来たという恐ろしいものです。またサーバ上の任意のファイルが読めたりもしました。

具体的には、MVG(MagickVectorGraphics) や SVG といったベクター画像ファイルに埋める描画コマンドのフィルタ漏れです。

  • 例えば CSS に http:〜 のような URL を指定すると Delegate 機能で curl を呼び出すのですが、「https://example.com/foo.jpg"|ls "-la」 とかで実行させられました。典型的なインジェクションです。
  • @/etc/passwd でそのファイルを読める事自体は仕様ですが、描画コマンドで label:@/etc/passwd すると、そのファイル内の文字列を描画した画像が。。

どちらも policy.xml で回避できます。これを機にファイル形式を絞りはじめたサイトも多かったのでは無いでしょうか。

クリティカルヒット(2) Yahoobleed (CVE-2017-9098)

自分が特に痛いと思うのはこちらです。メモリ初期化が不十分で前に処理した画像ファイルを読み出せる不具合。Yahoo!メールで他の人の画像(らしきもの)が読めてしまい、Yahoo さんはこれで ImageMagick から離れました。

当然この件の修正はされていますが、そんな不具合が入るソフトウェアが根本的に信用できるかという話になりますよね。。

なお、こちらも policy.xml で回避は出来ます。とも言い切れない少し複雑な事情があるので解説します。

Yahoobleed 解説

この脆弱性は RLE ファイル読み込みの不具合です。殆どの人は RLE を知らないと思うので公式ページURLを貼っておきます。

以下の18バイトの RLE ファイルを(多分 .jpg とかの拡張子で) Yahoo!メールに添付するだけで攻撃出来たようです。

52 CC:       header
00 00 00 00: top, left at 0x0.
00 04 00 04: image dimensions 1024 x 1024
02:          flags 0x02 (no background color)
01:          1 plane (i.e. grayscale image)
08:          8 bits per sample
00 00 00:    no color maps, 0 colormap length, padding
07:          end of image (a protocol command is consumed pre-loop)
07:          end of image (end the decode loop for real)

ImageMagick の RLE デコーダはこの条件の時にメモリ初期化を忘れて、前の画像データを再利用していました。Yahoo!メールはそれを JPEG 画像に変換して閲覧できるという流れです。
実際に攻撃して取得したとされる画像を並べます。

yahoo_letter_a_anon.jpg yahoo_leak_512_1_anonymized.jpg yahoo_leak_cmy_bands.jpg
yahoo_letter_a_anon.jpg yahoo_leak_512_1_anonymized.jpg yahoo_leak_cmy_bands.jpg

未初期化のメモリに残っている画像データの空間サイズや色空間は分からないので、そのものずばりの画像は見えません。しかし運が良ければ(悪ければ)復元できる可能性はあります。

例えば yahoo_letter_a_anon.jpg の width と stride(1 scanline 辺りの内部データ長) をずらすと

a_planes.png (width:256 stride:257) a_planes2.png (width:256 stride:172)
a_planes.png a_planes2.png

RGB の3チャネルが Planar 形式で並んでると仮定すると、

% convert a_planes.png -crop +14+0 +repage -crop 86x \
                     -set colorspace sRGB -combine a_color.png
% (a_planes2.png も同様)
a_color.png a_color2.png
a_color.png a_color2.png

(通常、RGB は Packed 形式で、Planar 形式だと YCbCr を真っ先に連想しますが、このデータだと YCbCr で復元しても駄目そうでした)

余談ですが、〜bleed の命名は、2012年の Heartbleed (OpenSSL 脆弱性)、2017年2月の Cloudbleed (Cloudinary reverse proxy 脆弱性)の流れで付いたと思われます。

Yahoobleed で思うところ

後出しジャンケンですが、RLE なんて太古のマイナーな形式を Yahoo!メールに添付する必要性はなさそうで、ブロックしておけば問題なかったのですが残念です。ImageMagick の ChangeLog を追っていればマイナーな画像形式の不具合修正が頻繁にあってシグネチャのチェックしないの怖いと思うでしょう。(大規模サイトならそういうチェックをしてる人がいるはず。きっと)

ただ、ImageMagick 開発者の立場だと、メモリ初期化漏れというたちの悪いミスで Yahoo さんにクリティカルな不具合を踏ませてしまったので、そんな逆ギレはとても口に出せません。

前の年に ImageTragick の問題が騒がれていたので、慎重なサイトでは処理する画像形式を絞っていたと思いますが、Yahoo さんでさえファイル形式を十分に絞れたなかったのを思うと、啓蒙が足りないのか、または現場の都合とかも色々思うところがあります。

でもまぁ、今からなら policy.xml で対処できますね。

コード実行脆弱性

コードを実行できる脆弱性は、こちらに一覧があります。

上記の脆弱性2つを含めて多くは policy.xml でファイル形式を限定すれば避けられますが、2016年の profile.c 脆弱性 (CVE-2016-5841) は避けられないので痛いです。。
脆弱性の影響をへらす事はできても、このようにヒットしてしまう事はあるので、ユーザから任意のファイルを受け取る以上は、脆弱性によるアップデートを追いかける必要があります。勿論、ImageMagick に限らず一般的な話です。

OSS-Fuzz の活動

2017年の脆弱性の多さは、OSS-Fuzz という優秀な脆弱性検査ツールで 2017年から徹底的に調べられているのが、大きな要因だと思われます。(セキュリティ強化月間的に、それ以外のチェックも色々してました。型のキャストを徹底するとか)

OSS-Fuzz については、こちらの記事をどうぞ.

以前の ImageMagick は脆弱性が多かったのは確かですが、OSS-Fuzz の徹底的なチェックもあって今は(以前に比べて)かなり安全になったとも言えます。

「PNG の変換画像が僅かに暗くなるケースがある」

サイボウズさんのブログでは、Go + disintegration/imaging でリサイズしたら、ImageMagick の画像より少し暗くなったそうです。

disintegration/imaging のリサイズ処理

画像に差異が出る要因として以下のものが思いあたります。尚、ここから disintegration/imaging を Imaging と呼びます。

  • 内部処理でのビット深度
    • ImageMagick の内部表現は(デフォルトで) Q16(= RGB48ビット)。
    • Imaging の内部表現は uint8 (= RGB24ビット)
  • リサイズのアルゴリズムは?
    • ImageMagick の縮小デフォルトは Lanczos3 。ただし、Sinc は多項式近似。
    • Imaging のサンプルが Lanczos3 なので恐らく、これを使ってる?
    • Imaging を CatmullRom フィルタ(BiCubic の一種)指定で使ってるようです。 これが暗くなる原因です
  • リサイズの縦横の扱い (実はこれで微妙に差異が出る、見た目には分からない程度)
    • ImageMagick は縦横同時に縮める
    • Imaging は横を縮めてから縦を縮める

補間カーネル

画像リサイズの縮小処理では色をどのように混ぜてピクセルを減らすのかで色んな手法があり、補間カーネルという重み付け数列で決める事が多いです。

詳しくは以前、書いた ImageMagick のリサイズ記事を参照して下さい。

サイボウズさんのブログに貼られていた画像ファイルを見ると、 ImageMagick は Lanczos(3)、Imaging は CatmullRom を使っているようです。
ImageMagick 6.9.7-5 以前で -resize を使うと画像が完全に一致します。
Imaging のフィルタはこちらのプログラムで確認しました > https://gist.github.com/yoya/4f2d074d909d21451bd1bc3be4b22338

Imaging の resize フィルタにデフォルト値はなく、サンプルは Lanczos(3) なので CatmullRom は何かしらの意図を持って選んだと推察されます。(なのになんで画像の差分があるのに驚いているのかは不思議。。)

各々の補間カーネルの数列をグラフにして貼ります。

Lanczos (Lobe:3)
image.png
CatmullRom (BiCubic B:0,C:0.5)
image.png

CatmullRom の方が山の丸みが少なくて面積が少な目で、これで畳み込むと暗くなりそうです。なお、Lanczos の波ののたうちは LPF(ローパスフィルタ) の効果を持ちます。CatmullRom でそれが少ないという事はエイリアシングの懸念があります。見た目が明瞭になる傾向のフィルタですので、この判断はなかなか玄人好みの感じがします(扱いが難しいという意味でも)。

リサイズの縦横の扱い

蛇足ですが。参考までに。
Imaging は横をリサイズしてから縦をリサイズしますが、ImageMagick は横長のときに横を先にリサイズ、縦長のときは縦を先にリサイズします。

if (x_factor > y_factor)
  {
    span=(MagickSizeType) (filter_image->columns+rows);
    status=HorizontalFilter(resize_filter,image,filter_image,x_factor,span,
      &offset,exception);
    status&=VerticalFilter(resize_filter,filter_image,resize_image,y_factor,
      span,&offset,exception);
  }
else
  {
  span=(MagickSizeType) (filter_image->rows+columns);
  status=VerticalFilter(resize_filter,image,filter_image,y_factor,span,
    &offset,exception);
  status&=HorizontalFilter(resize_filter,filter_image,resize_image,x_factor,
    span,&offset,exception);
}

(ツイッターで呟いた時は、その挙動から ImageMagick は 2D畳み込みだと勘違いしてました。すみません)

使い捨てのサムネール画像を生成する分には問題ないですが、そこから更に強調加工して表示する場合に違いが問題になる事もあります。(医療用画像とかそうですね。レントゲンやCTで病変が消えたり、逆に偽の像で誤診とか恐ろしい。。)

その他気になる点

むしろ、そんな小さな違いよりメタデータの処理が気にかかります。
ImageMagick は画像のメタデータにあるガンマ値やICCプロファイル、Exif Orientation(縦横回転)等の補正に対応しますが、Imaging だと恐らく自力で頑張る必要がありそうです。あと CMYK は RGB に変換してあげなきゃとか、それらの組み合わせで ImageMagick がさんざん不具合を出して乗り越えてきたように、難易度高めです。頑張って下さい。。

また、JPEG や PNG, GIF のファイル形式読み込みは Go の image パッケージを使うと思いますが、残念ながら jpeglib や libpng 程の仕様カバー率はありません、そこそこデコードが怪しいです。更にこれから WebP や HEIF 等のサポートも求められると 各々 VP8, H265/HEVC の動画デコーダも要るので Go で実装するの大変そうですね。

画像が投稿できない、または投稿した画像の色や向きが変になった場合に、ユーザ自身が頑張って素直な形式に変換して投稿し直して頂く。というのは大なり小なりあるので、利便性と脆弱性のトレードオフではあるとは思います。

「変換できない画像がある」

BMPv4, v5 の対応

しかし Go の image パッケージの当該部分を見ると 40 という値が来ることを前提としており、
Windows V3 以外の BMP のバージョンは未サポートでした。

(Windows V3 の表現は馴染みがないですが、日本語版 Wikipedia の文章から BMP version3 の事だと解釈します。以下 BMPv3 と略)
Windows のペイントツールで保存すると BMPv3 が出力されるので v4,v5 未対応でも良い。みたいな判断も有りそうですが、例えば ImageMagick だと大抵 BMPv5 相当が出力されるといった具合で、少し調査が必要な案件ですね。

BMP ファイルは以下のような構造をしています。

image.png
参照元) https://qiita.com/yoya/items/3d588687a30175601885 ImageMagick で BMP処理

BMP 対応を謳うのであれば、折角ですし Go に v4,v5 対応をコントリビュートしてあげると良いのではないでしょうか。
v4 から RGBmask と XYZ補正、v5 から ICC プロファイル対応なので、ちょっと大変そうです。

「ImageMagick は壊れた画像も変換できるという謎機能があり」

他にも、ImageMagick は壊れた画像も変換できるという謎機能があり、もちろん壊れ方にも依るのですが、
Go 版では変換に失敗する画像でも ImageMagick だと変換できることがあります。
なおそういったケースは全体から見れば極僅かなのでこのケースも実用では問題無いでしょう。

これは謎という訳でもなく、信号処理での一般的な手法です。世の中お行儀の良いデータばかりでは無いので。。

例えば

  • 1箇所でも壊れていたら全く表示しない
  • 読めた分は表示する
  • 壊れた場所を無視またはデフォルト値扱いしてできる限り復元して表示する

など対応の具合は様々です。勿論、処理を減らすほど脆弱性は減りますが、Webブラウザも結構頑張りますし。そこはバランス次第です。

最後に

自分も ImageMagick を使わない方が良いと判断した仕事をいくつか持っていますが、脆弱性を理由にする事はあまりなくて、

  • メモリを大量に使う。そして重たい。
  • 画像をストリーム的に扱えない。オンザフライ出来ない。
  • 扱えない画像形式もある。HEIC は最近まで扱えなかったし。

といった点で、サービスレベルを落とせる場合は、サイボウズさんのように Go + 何かのライブラリで頑張る事もあります。

あと、ImageMagick といえど何でもは出来ないので、目的に応じて使い分けましょう。

  • 画像認識の機能がほしい > OpenCV
  • 機械学習系の処理がほしい > PIL(Pillow) や scipy
  • 動画やりたい > ffmpeg
  • 高度なベクタ画像処理 > Cairo

といった具合です。但し、ImageMagick でないからといって脆弱性と無縁ではないので、そっちはそっちで気をつけて下さい。 > 当記事の mala さんのコメント参照

自分は ImageMagick リリースの度に全差分見ているので、怪しいと思ったら更新止めたりしてます。

コードの肥大化が心配されますが、殆どの脆弱性が画像処理のコア部分でなく、枝場のマイナーな画像形式読み書きに集中している事から policy.xml で多くは切り離せるでしょう。むしろ最近は機能追加に保守的に見えますし、2017年,2018年前半の徹底したチェックがあって以前よりは堅牢性が増しています。但し、勿論、C言語で実装している時点で危険と言われれば、その通りです。

C言語で実装すると脆弱性が入りやすいのは実感としてあり、Go なり Rust なりで実装した ImageMagick に代わる決定的なソフトが出てこないかなといった期待はしてます。自分もサブセット的なのは仕事で作りますが、汎用的なのを作る機会はないですね。うーん。(ぶっちゃけ画像ファイルを安全に読み書きするのだけでも凄い大変なんです。。)

2018/08/28 追記

PIL(python-pillow) について mala さんから有用なコメントを頂きました。是非そちらも参照下さい。

言語ネイティブの画像処理ライブラリを使うためのヒント、信頼できない画像を処理するためのヒント。大変為になります、ありがとうございます。

2018/08/31 追記

より詳細な ImageMagick の使用上の注意、その対策、また ImageMagick を使わない場合の代案まで書かれた凄い記事を見つけたので紹介します。

いつの日か、こういう記事を書けるようになりたいです。