アップロードするファイル形式の検証不足

アップロードするファイル形式の検証不足

説明

拡張子、MIMEタイプ、ファイルのシグネチャーなどを許可リストで検証せずにアップロードを受け付けると、悪意のあるファイルがサーバーに保存されるおそれがあります。

想定される影響

  • 実行可能なファイルやスクリプトが、サーバーのファイルシステムに保存される可能性があります。
  • 公開ディレクトリに置いたファイルが、悪意のあるコンテンツとしてユーザーへ配信されるおそれがあります。

対処方法

  • 拡張子、MIMEタイプ、ファイルのシグネチャーを許可リストで検証してください。
  • ファイル名はサーバーで生成し、保存先を実行用のパスから分離してください。
  • multer の fileFilter と別のコンテンツ検証を組み合わせてください。

例

Expressの app と隔離ディレクトリは準備済みとします。変更前の upload にはファイル形式の検証がない前提です。

変更前

javascript
app.post("/upload", upload.single("file"), (req, res) => {
  res.json({ path: req.file.path });
});

変更後

javascript
const crypto = require("crypto");
const fs = require("fs/promises");
const path = require("path");
const multer = require("multer");

// Web公開・実行用のパスではなく、他のユーザーが書き込めない隔離ディレクトリ
const QUARANTINE_DIR = "/srv/app/upload-quarantine";
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);

const upload = multer({
  storage: multer.diskStorage({
    destination: QUARANTINE_DIR,
    filename(req, file, cb) {
      cb(null, `${crypto.randomUUID()}.png`);
    },
  }),
  limits: { fileSize: 5 * 1024 * 1024 },
  fileFilter(req, file, cb) {
    const extension = path.extname(file.originalname).toLowerCase();
    const allowed = file.mimetype === "image/png" && extension === ".png";
    cb(null, allowed);
  },
});

async function hasPngSignature(filePath) {
  const handle = await fs.open(filePath, "r");
  try {
    const header = Buffer.alloc(PNG_SIGNATURE.length);
    const { bytesRead } = await handle.read(header, 0, header.length, 0);
    return bytesRead === header.length && header.equals(PNG_SIGNATURE);
  } finally {
    await handle.close();
  }
}

app.post("/upload", upload.single("file"), async (req, res, next) => {
  if (!req.file) return res.status(400).send("invalid file type");

  try {
    if (!(await hasPngSignature(req.file.path))) {
      await fs.unlink(req.file.path);
      return res.status(400).send("invalid file type");
    }
    return res.json({ id: req.file.filename });
  } catch (error) {
    await fs.unlink(req.file.path).catch(() => {});
    return next(error);
  }
});

解説:

  • 変更前: 検証しないアップロードによって、悪意のあるスクリプトや実行可能ファイルが保存されるおそれがあります。
  • 変更後: サイズ、拡張子、MIMEタイプ、PNGシグネチャーを検証し、生成した名前で非公開の隔離領域に保存します。フィルターによる拒否で req.file がない場合を処理し、保存後の検証失敗やエラーでは一時ファイルを削除します。PNGシグネチャーだけで全体の有効性や無害性は保証できません。必要に応じて画像のデコード・再エンコードなど、用途に合う検証を追加してください。

参考資料