アップロードされたファイルが正常な画像ファイルかどうかをチェックする(Node.js)


「呼び出し元からアップロードされた画像ファイルが正常な画像ファイルか」を、サーバーサイドで ある程度チェックしたくなりました。(Node.js)

他の方がすでに実現してるんだろうな、と思って探してみたものの意外に見つからず・・。しょうがないので実装してみた次第です。

やりたいこと

私の場合、後続のビジネスロジックで 「呼び出し元から入力されたファイルが、画像として開ける」 ということを前提としていました。

その前提を満たすためには、ビジネスロジックの前に以下の入力値チェックが必要と考えました。

  • 受け取ったファイルのマジックナンバーが正当かチェック。例えばjpegであれば、開始バイトはff d8(16進数)であるはずです。
  • 受け取ったファイルの中身が壊れていないかチェック。
  • 受け取ったファイルの中身が、画像と全く異なるものでないかチェック(攻撃スクリプトを装っていないか)。

これ以上のチェックについては、別タイミングで実行するウイルスチェックに任せます。例えば、jpegのExifにスクリプトが仕込まれていないか(参考ページ)、といったことまではチェックしません。Exifのチェックをすることもできるのですが、中途半端ですし、そもそも目的から外れます。

なお、呼び出し元が指定するファイル名や拡張子、Content-Typeヘッダーの中身は信用できませんので、それらをチェックしても意味がありません。

また、HTTPリクエストサイズの上限チェックなど、他のチェック処理も当然ながら必要です。この記事では、そういったことは省略します。

実装

かなり広く使われている模様のsharpを使って、実装してみました。

index.js
'use strict'

const sharp = require('sharp')

// ここでチェックしています。
const check_image_validity = file_path => sharp(file_path).toBuffer()

const image_file_names = ['valid.jpg', 
                          'invalid_magic_number.jpg', // [Error: Input file contains unsupported image format]
                          'jpeg_magic_number_but_corrupted.jpg'] //[Error: Input file has corrupt header: VipsJpeg: Corrupt JPEG data: 597 extraneous bytes before marker 0xd4 VipsJpeg: JPEG datastream contains no image]

image_file_names.forEach(file_name => {
  check_image_validity('input_images/' + file_name)
    .then(() => console.log('it is a valid image! file name:', file_name))
    .catch(err => console.log('invalid image detected. file name:', file_name, ' error:', err))
})

sharpでファイルを読み込み、sharpのtoBuffer()でメモリに書き出しています。この時、エラーが起こるのであれば、入力した画像ファイルが不正ということです。

コメントの通り、valid.jpg以外のファイルでは、エラーが発生します。不正な画像ファイルを良い感じに検知できているというわけです。

  • valid.jpg : 正当なJPEGファイル
  • invalid_magic_number.jpg : マジックナンバー(冒頭のバイト列)が不正なファイル
  • jpeg_magic_number_but_corrupted.jpg : マジックナンバーはJPEGのものだが、中身が不正なファイル。

ソースコード