アップロードファイル名の検証不足

アップロードファイル名の検証不足

説明

送信されたファイル名を保存パスに使うと、攻撃者が意図しない場所へファイルを保存する可能性があります。../や..\などのパストラバーサル文字列を含む名前は、保存先を指定ディレクトリの外へ向けるおそれがあります。

想定される影響

  • 意図しないファイル作成: 操作された名前によって別の保存先が選ばれる可能性があります。
  • ファイルの上書き: 書き込みAPIが置換を許し、プロセスに権限がある場合、既存ファイルを損なうおそれがあります。
  • コード実行: サーバーがアップロード内容を実行する構成なら、コード実行につながる可能性があります。保存するだけで実行されるわけではありません。

対処方法

  1. 送信された名前を直接使わず、UUIDなどに基づく保存名をサーバーで生成してください。
  2. 意図したファイル名の形式だけを受け入れてください。パス文字を単に削除するのではなく、空の名前や.・..などを拒否してください。^[a-zA-Z0-9._-]+$のような許可パターンは検証の一部です。
  3. /var/app/uploadsなど管理されたディレクトリに保存し、パスがその範囲内に収まることを確認してください。

例

javax.servletによるアップロード処理の抜粋です。multipart設定、認証・認可、CSRF保護、サイズ・内容の検証、エラー処理は別途構成してください。uploadsは実行可能なWebパスの外に作るサーバー管理ディレクトリで、他のユーザーによる変更を防ぐ必要があります。.txt拡張子は内容検証の代わりにはなりません。

変更前

java
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Part filePart = request.getPart("file");
String fileName = filePart.getSubmittedFileName(); // 送信された名前を直接使用
Files.copy(filePart.getInputStream(), Paths.get("uploads", fileName));
}
}

変更後

java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;

@WebServlet("/upload")
public class SecureFileUploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Part filePart = request.getPart("file");
String fileName = filePart.getSubmittedFileName();

        if (fileName == null || fileName.equals(".") || fileName.equals("..") || !isFileNameAllowed(fileName)) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid file name");
            return;
        }

        // 保存名をサーバーで生成
        String storedFileName = UUID.randomUUID().toString() + ".txt";
        Path targetPath = Paths.get("uploads").resolve(storedFileName);
        Files.copy(filePart.getInputStream(), targetPath);
    }

    private boolean isFileNameAllowed(String fileName) {
        return fileName.matches("^[a-zA-Z0-9._-]+$"); // 許可したファイル名パターンを確認
    }
}

説明:

  • 変更前: 送信された名前を保存パスに使い、書き込み可能な意図しない場所へ新しいファイルを作るおそれがあります。このFiles.copy()は既存の保存先があると既定で失敗し、保存先のファイルを読み取る例ではありません。
  • 変更後: 未指定、空、特殊な名前を拒否し、許可パターンを確認します。プラットフォーム別の区切り文字の削除に依存しません。実際の保存名はサーバーが生成したUUIDから決まります。

参考資料