zukucode
主にWEB関連の情報を技術メモとして発信しています。

ASP.NET Coreで画像ファイルを安全にアップロードする

画像を受け取るAPIでは、拡張子だけを信頼して保存すると、想定外の内容や巨大なファイルを受け入れる可能性があります。ここでは小さな画像を1件受け取る基本と、最低限確認したい境界を扱います。

multipart/form-dataで受け取る

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/images")]
public sealed class ImagesController(IWebHostEnvironment environment) : ControllerBase
{
    private const long MaxFileSize = 2 * 1024 * 1024;
    private const long MaxRequestSize = 3 * 1024 * 1024;

    [HttpPost]
    [RequestSizeLimit(MaxRequestSize)]
    public async Task<IActionResult> Upload(
        [FromForm] IFormFile file,
        CancellationToken cancellationToken)
    {
        if (file.Length == 0 || file.Length > MaxFileSize)
            return BadRequest("2MB以下の画像を指定してください。");

        if (file.ContentType is not ("image/jpeg" or "image/png"))
            return BadRequest("JPEGまたはPNGを指定してください。");

        var extension = file.ContentType == "image/png" ? ".png" : ".jpg";
        var fileName = $"{Guid.NewGuid():N}{extension}";
        var directory = Path.Combine(environment.ContentRootPath, "uploads");
        Directory.CreateDirectory(directory);
        var path = Path.Combine(directory, fileName);

        await using var output = System.IO.File.Create(path);
        await file.CopyToAsync(output, cancellationToken);

        return Created($"/api/images/{fileName}", new { FileName = fileName });
    }
}

フォームのフィールド名はAction引数のfileに合わせます。

curl -X POST http://localhost:5000/api/images \
  -F 'file=@sample.png'

元のファイル名を保存名にしない

IFormFile.FileNameは利用者が送った値です。パスや同名ファイルの問題を避けるため、保存先には使わず、サーバー側で名前を生成します。元の名前を表示用に残す場合も、文字数などを検証してメタデータとして保存します。

Content-Typeだけでは保証できない

ContentTypeもクライアントが指定できます。本番で画像として公開・加工する場合は、画像ライブラリでデコードできるか確認し、許可した形式へ再エンコードする方法を検討します。必要なら公開前の隔離領域とウイルス検査も用意します。

保存先を選ぶ

ローカルファイルは複数サーバー間で共有されず、再デプロイで失われる環境があります。本番ではオブジェクトストレージなど永続化された保存先を検討します。検証前のファイルを公開ディレクトリへ直接保存しないことも重要です。

multipartにはファイル以外の情報も含まれるため、リクエスト全体の上限はファイル上限より少し大きくしています。Actionだけでなく、Webサーバーやリバースプロキシにも上限があります。


関連記事