准则
文件上传
通常情况下,应用程序会在某些时候需要允许用户在应用程序中的某个地方上传文件(无论是使用还是存储)。虽然这看起来很简单,但由于文件上传的处理方式存在潜在风险,因此如何实现这一功能可能相当关键。
请看这个简单的例子,以便更直观地理解我们的意思。
假设这是一个允许用户上传个人照片的应用程序:
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));
}