ファイルのアップロード
アプリケーションは、ある時点で、ユーザがアプリケーション内のどこかにファイルをアップロードできるようにする必 要が生じることがよくあります(使用するため、あるいは単に保存するため)。単純なことのように見えますが、この機能をどのように実装するかは、ファイルアップロードがどのように扱われるか に関連する潜在的なリスクのために、非常に重要です。
この簡単な例を見て、私たちが言いたいことをより視覚的に理解してほしい。
例えば、ユーザーがプロフィール写真をアップロードできるアプリケーションだとしよう:
public string UploadProfilePicture(FormFile uploadedFile)
{
// Generate path to save the uploaded file at
var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";
// Save the file
var localFile = File.OpenWrite(path);
localFile.Write(uploadedFile.ReadToEnd());
localFile.Flush();
localFile.Close();
// Update the profile picture
UserProfile.UpdateUserProfilePicture(request.User, path)
return path;
}
これは非常に基本的なアップロード機能であり、パストラバーサルに対して脆弱である。
アプリケーションの正確な実装に依存しますが、攻撃者は別のページ/スクリプト(.asp、.aspx、.php ファイルを考えて下さい)をアップロードすることが可能です。また、既存のファイルを上書きすることも可能です。
問題1 - 外部データストアではなくローカルディスクに保存する
クラウドサービスの利用が一般的になるにつれ、アプリケーションはコンテナで提供され、高可用性セットアップが標準となり、アップロードされたファイルをアプリケーションのローカルディスクに書き込む習慣は、基本的に何としても避けるべきである。
ファイルは、可能な限り中央のストレージ(ブロックストレージ、またはデータベース)にアップロードすべきである。この場合、セキュリティの脆弱性のクラス全体を回避することができる。
問題2 - エクステンションが検証されない
ファイルアップロードの脆弱性が悪用される多くの場合、特定の拡張子を持つファイルをアップロードする能力に依存している。そのため、アップロード可能なファイルの拡張子の「許可リスト」を利用することを強くお勧めします。
ヌル・バイト・インジェクションのような問題を避けるために、ファイルの拡張子を取得するために、あなたの言語/フレームワークが提供するメソッドを必ず使用してください。
アップロードのコンテントタイプを検証することも魅力的かもしれませんが、特定のファイルに使用されるコンテントタイプがオペレーティングシステム間で異なる可能性があることを考えると、そうすることは非常にもろくなります。また、コンテンツタイプは純粋に拡張子からのマッピングであるため、実際にはファイル自体について何も教えてくれません。
問題3 - パスのトラバーサルを防止できない
ファイルアップロードのもう一つの一般的な問題は、パストラバーサルに対する脆弱性です。これはそれ自体で完結することなので、ここで要約するよりも、パストラバーサルに関するガイドラインの全文をご覧ください。
その他の例
以下に、安全なファイルアップロードと安全でないファイルアップロードの例を示します。
C# - 安全ではない
public string UploadProfilePicture(IFormFile uploadedFile)
{
// Generate path to save the uploaded file at
var path = $"./uploads/avatars/{request.User.Id}/{uploadedFile.FileName}";
// Save the file
var localFile = File.OpenWrite(path);
localFile.Write(uploadedFile.ReadToEnd());
localFile.Flush();
localFile.Close();
// Update the profile picture
UserProfile.UpdateUserProfilePicture(request.User, path)
return path;
}
C# - セキュア
public List<string> AllowedExtensions = new() { ".png", ".jpg", ".gif"};
public string UploadProfilePicture(IFormFile uploadedFile)
{
// NOTE: The best option is to avoid saving files to the local disk.
var basePath = Path.GetFullPath("./uploads/avatars/");
// Prevent path traversal by not utilizing the provided file name. Also needed to avoid filename conflicts.
var newFileName = GenerateFileName(uploadedFile.FileName);
// Generate path to save the uploaded file at
var canonicalPath = Path.Combine(basePath, newFileName);
// Ensure that we did not accidentally save to a folder outside of the base folder
if(!canonicalPath.StartsWith(basePath))
{
return BadRequest("Attempted to save file outside of upload folder");
}
// Ensure only allowed extensions are saved
if(!IsFileAllowedExtension(uploadedAllowedExtensions))
{
return BadRequest("Extension is not allowed");
}
// Save the file
var localFile = File.OpenWrite(canonicalPath);
localFile.Write(uploadedFile.ReadToEnd());
localFile.Flush();
localFile.Close();
// Update the profile picture
UserProfile.UpdateUserProfilePicture(request.User, canonicalPath)
return path;
public bool GenerateFileName(string originalFileName) {
return $"{Guid.NewGuid()}{Path.GetExtension(originalFileName)}";
}
public bool IsFileAllowedExtension(string fileName, List<string> extensions) {
return extensions.Contains(Path.GetExtension(fileName));
}