在用户上传文件时,有可能会遭受到攻击:
降低成功攻击可能性的安全措施如下:
数据库存储:
物理存储:
云数据存储服务
先将整个文件保存到内存,然后通过IFormFile得到stream。如果并发文件上传的数量和大小超过了内存的容量,网站就会因为内存不足而崩溃。
将一个stream分多个Section读取并写入,无需将整个请求放入内存。流式传输无法显著提高性能。 流式传输可降低上传文件时对内存或磁盘空间的需求。
完整代码:github代码路径
由于Kestrel最大请求正文限制在28.6M,在Program.cs里更改最大请求正文限制(bytes)。
var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(serverOptions => {
serverOptions.Limits.MaxRequestBodySize = 524288000;
});
[HttpPost]
[Route("upload")]
public async Task Upload()
{
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
ModelState.AddModelError("File",
"无法处理该请求(ContentType不是Multipart).");
_logger.LogError("无法处理该请求(ContentType不是Multipart).");
return BadRequest();
}
var boundary = MultipartRequestHelper.GetBoundary(
MediaTypeHeaderValue.Parse(Request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// 如果存在表单数据,立即失败并返回。
if (!MultipartRequestHelper
.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File", $"无法处理该请求(ContentDisposition不是form-data,或文件名为空)");
_logger.LogError("无法处理该请求(ContentDisposition不是form-data,或文件名为空)");
return BadRequest(ModelState);
}
else
{
// 不要相信客户端发送的文件名。 要显示文件名,请对值进行 HTML 编码。
var trustedFileNameForDisplay = WebUtility.HtmlEncode(
contentDisposition.FileName.Value);
var trustedFileNameForFileStorage = Path.GetRandomFileName();
// **警告!**
// 在以下文件处理方法中,不会扫描文件的内容。
// 在大多数生产场景中,在将文件提供给用户或其他系统之前,
// 会在文件上使用防病毒/反恶意软件扫描程序 API。
var streamedFileContent = await FileHelpers.ProcessStreamedFile(
section, contentDisposition, ModelState,
_permittedExtensions, _fileSizeLimit, _logger);
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var targetStream = System.IO.File.Create(
Path.Combine(_targetFilePath, trustedFileNameForFileStorage)))
{
await targetStream.WriteAsync(streamedFileContent);
_logger.LogInformation(
"文件'{TrustedFileNameForDisplay}' 保存在 " +
"'{TargetFilePath}' 作为 {TrustedFileNameForFileStorage}",
trustedFileNameForDisplay, _targetFilePath,
trustedFileNameForFileStorage);
}
}
}
// 读取下一节
section = await reader.ReadNextSectionAsync();
}
return Created(nameof(FileUploadController), null);
}
需验证文件的扩展名,将不能上传的文件类型排除在外。
var ext = Path.GetExtension(fileName).ToLowerInvariant();
if (string.IsNullOrEmpty(ext) || !permittedExtensions.Contains(ext))
{
return false;
}
文件的签名由文件开头部分中的前几个字节确定。 可以使用这些字节指示扩展名是否与文件内容匹配。 下面是常见压缩文件的示例。
private static readonly Dictionary> _fileSignature = new Dictionary>
{
{ ".zip", new List
{
new byte[] { 0x50, 0x4B, 0x03, 0x04 },
new byte[] { 0x50, 0x4B, 0x4C, 0x49, 0x54, 0x45 },
new byte[] { 0x50, 0x4B, 0x53, 0x70, 0x58 },
new byte[] { 0x50, 0x4B, 0x05, 0x06 },
new byte[] { 0x50, 0x4B, 0x07, 0x08 },
new byte[] { 0x57, 0x69, 0x6E, 0x5A, 0x69, 0x70 },
}
},
{ ".rar", new List { new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07,0x00 } } },
{ ".7z", new List{ new byte[] { 0x37,0x7A,0xBC,0xAF,0x27,0x1C } } },
}
using (var reader = new BinaryReader(data))
{
// 文件签名检查
var signatures = _fileSignature[ext];
var headerBytes = reader.ReadBytes(signatures.Max(m => m.Length));
}
尽量不要使用客户端提供的文件名,作为文件存储的文件名。使用 Path.GetRandomFileName
或 Path.GetTempFileName
为文件创建安全的文件名。
如果文件重名的情况下,不覆盖文件,需要在数据库中查询文件是否重名。
限制上传的文件的大小。
appsetting.json文件内容
{
"FileSizeLimit": 524288000
}
文件大小超出限制时,将拒绝文件
if (memoryStream.Length > sizeLimit)
{
// 文件过大的处理...
}