Java Tips | 基本ユーティリティ:MIMEタイプ判定

Java Java
スポンサーリンク

MIMEタイプ判定は「中身の種類をちゃんと確認する」ための技

拡張子はあくまで「ラベル」でしかなくて、virus.exevirus.png にリネームすることも簡単にできます。
業務システムでファイルを扱うときは、「本当に画像なのか?」「本当に CSV なのか?」を、できるだけ中身ベースで確認したくなります。

そこで出てくるのが「MIMEタイプ判定」です。
image/pngtext/csv のような「コンテンツタイプ」を調べることで、拡張子だけに頼らない、少しマシなチェックができるようになります。


基本:Files.probeContentType(Path) で MIMEタイプを推測する

まずは標準APIでの最小サンプル

Java 7 以降には、標準で MIMEタイプを推測する API が用意されています。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class MimeBasic {

    public static void main(String[] args) throws IOException {
        Path path = Path.of("data/sample.png");
        String mime = Files.probeContentType(path);

        System.out.println("file = " + path);
        System.out.println("mime = " + mime);
    }
}
Java

Files.probeContentType(path) は、OS やインストールされているファイルタイプ情報を使って、
そのファイルの MIMEタイプ(例: image/png, text/plain)を文字列で返してくれます。
判定できなかった場合は null が返ることもあります。

ここで重要なのは、「必ず正しいとは限らないが、“拡張子だけよりはマシ”な判定ができる」という位置づけです。
OS の設定や環境に依存する部分もあるので、「絶対に信用してよい真実」ではなく、「判断材料の一つ」として使う感覚が大事です。


実務で使える MIMEタイプ判定ユーティリティの最小形

null や例外を吸収して、扱いやすい形にする

probeContentTypeIOException を投げる可能性があり、判定できないときは null になります。
そのまま使うと呼び出し側が毎回面倒なので、ユーティリティで包んでしまいます。

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;

public final class MimeTypes {

    private MimeTypes() {}

    public static Optional<String> detect(Path path) {
        if (path == null) {
            return Optional.empty();
        }
        try {
            String mime = Files.probeContentType(path);
            return Optional.ofNullable(mime);
        } catch (IOException e) {
            return Optional.empty();
        }
    }

    public static String detectOrUnknown(Path path) {
        return detect(path).orElse("application/octet-stream");
    }
}
Java

使う側はこう書けます。

Path path = Path.of("data/sample.png");

System.out.println(MimeTypes.detect(path));          // Optional[image/png] など
System.out.println(MimeTypes.detectOrUnknown(path)); // image/png または application/octet-stream
Java

ここで深掘りしたいポイントは二つです。

一つ目は、「判定できないケースを Optional やデフォルト値で吸収している」ことです。
呼び出し側が毎回 try-catchnull チェックを書くのはつらいので、ユーティリティ側で“扱いやすい形”に変換しておきます。

二つ目は、「application/octet-stream を“よく分からないバイナリ”のデフォルトとして使う」ことです。
これは「特定のタイプが分からないバイナリデータ」を表す MIMEタイプで、
「とりあえず何かのバイナリだけど、詳細は不明」という意味合いでよく使われます。


例題:アップロード画像の MIMEタイプチェック

拡張子+MIMEタイプの二段構えで弾く

画像アップロード機能を考えてみます。
「拡張子が .png.jpg であること」に加えて、「MIMEタイプが image/ で始まること」も確認すると、
単純な偽装をある程度は防げます。

import java.nio.file.Path;
import java.util.Set;

public final class ImageUploadValidator {

    private ImageUploadValidator() {}

    private static final Set<String> ALLOWED_EXT = Set.of("png", "jpg", "jpeg");

    public static void validate(Path uploadedFile) {
        String ext = PathExtensions.getExtension(uploadedFile).toLowerCase();
        if (!ALLOWED_EXT.contains(ext)) {
            throw new IllegalArgumentException("許可されていない拡張子です: " + ext);
        }

        String mime = MimeTypes.detectOrUnknown(uploadedFile);
        if (!mime.startsWith("image/")) {
            throw new IllegalArgumentException("画像ではない可能性があります: mime=" + mime);
        }
    }
}
Java

使い方はこうです。

Path uploaded = Path.of("work/upload/user-icon.png");
ImageUploadValidator.validate(uploaded);
Java

ここでの重要ポイントは、「拡張子と MIMEタイプを“両方見る”ことで、チェックの精度を上げている」ことです。
拡張子だけだと簡単に偽装されますし、MIMEタイプだけだと環境依存で判定できないこともあります。
二つを組み合わせることで、「安価にできる範囲で、そこそこマシなチェック」を実現しています。

もちろん、これでも完璧ではありませんが、「何も見ない」よりはずっと安全側に寄せられます。


例題:ログに MIMEタイプを出しておく

障害調査で「何を扱っていたか」を一目で分かるようにする

ファイル処理の障害が起きたとき、「そのとき扱っていたファイルがどんな種類だったか」が分かると、原因に近づきやすくなります。
そこで、「ファイルパス+サイズ+MIMEタイプ」をまとめてログに出すユーティリティを用意しておくと便利です。

import java.io.IOException;
import java.nio.file.Path;

public final class FileDescribe {

    private FileDescribe() {}

    public static String describe(Path path) throws IOException {
        String mime = MimeTypes.detectOrUnknown(path);
        long size = FileSizes.size(path);
        return path + " [" + mime + ", " + FileSizes.humanReadable(size) + "]";
    }
}
Java

使い方はこうです。

Path file = Path.of("data/input.csv");
System.out.println("processing " + FileDescribe.describe(file));
Java

これでログには、例えばこんな感じで出ます。

processing data/input.csv [text/plain, 12.3 KB]

ここでのポイントは、「MIMEタイプを“ログの文脈情報”として使う」ことです。
「text/plain だと思っていたけど、実は application/zip だった」というようなミスマッチが、ログからすぐに見えるようになります。


サードパーティ(Apache Tika など)を使う選択肢

本気でやるなら「中身を解析するライブラリ」

標準の Files.probeContentType は、OS の設定や拡張子ベースの判定に依存することが多く、
「ファイルの中身をしっかり解析して判定する」ほどの精度はありません。

もっと本格的に MIMEタイプを判定したい場合は、Apache Tika のようなライブラリを使う選択肢があります。

import org.apache.tika.Tika;

public final class TikaMimeTypes {

    private static final Tika tika = new Tika();

    public static String detect(byte[] data) {
        return tika.detect(data);
    }
}
Java

Tika はファイルのシグネチャ(先頭のバイト列)などを見て判定してくれるので、
拡張子に頼らない、より精度の高い MIMEタイプ判定ができます。

ただし、依存ライブラリが増える・処理が重くなるといったトレードオフもあるので、
「どこまでの精度が必要か」「どの処理にだけ使うか」をチームで決めて導入するのが現実的です。


MIMEタイプ判定の“限界”と注意点

「判定結果を過信しない」ことが一番大事

MIMEタイプ判定は便利ですが、「100% 正しいとは限らない」という前提を絶対に忘れてはいけません。
標準 API でも Tika でも、「判定ロジックの限界」「壊れたファイル」「意図的な偽装」などで、誤判定は起こり得ます。

だからこそ、次のようなスタンスが大事です。

  • MIMEタイプは「許可するかどうかの一条件」であって、「安全性を完全に保証するもの」ではない。
  • アップロード機能などでは、MIMEタイプチェックに加えて「サイズ上限」「拡張子」「中身のパース」など、複数の防御線を張る。
  • 判定できなかった(null や application/octet-stream)場合の扱いを、仕様として決めておく。

この「過信しない」という感覚を持っていれば、MIMEタイプ判定はとても頼りになる“補助ツール”になります。


まとめ:MIMEタイプ判定ユーティリティで身につけたい感覚

MIMEタイプ判定は、「拡張子だけに頼らず、ファイルの“中身の種類”をできる範囲で確認する」ための技です。

押さえておきたい感覚はこうです。

  • 標準の Files.probeContentType(Path) をユーティリティで包み、null や例外を扱いやすくする。
  • 拡張子チェックと組み合わせて、「安価にできる範囲でそこそこマシなバリデーション」を行う。
  • ログには「パス+サイズ+MIMEタイプ」を出しておき、障害調査で状況を再現しやすくする。
  • 精度が本当に必要な箇所だけ、Apache Tika のようなライブラリ導入を検討する。
  • MIMEタイプ判定は“完全な安全”ではなく、“判断材料の一つ”として使う、という距離感を保つ。

タイトルとURLをコピーしました