文章目录
.net core 大文件上传
注意事项
在用户上传文件时,有可能会遭受到攻击:
- 执行拒绝服务攻击
- 上传病毒或恶意软件
- 以其他方式破坏网络和服务器
降低成功攻击可能性的安全措施如下:
- 将文件上传到专用文件上传区域,最好是非系统驱动器。 使用专用位置便于对上传的文件实施安全限制。 禁用对文件上传位置的执行权限;
- 保存文件的目录应与程序所在目录不一致;
- 使用应用确定的安全的文件名,不使用用户提供的文件名或上传的文件的不受信任的文件名;
- 限制文件的扩展名;
- 验证是否对服务器执行客户端检查。客户端检查易于规避;
- 限制上传文件的大小;
- 文件不应该被具有相同名称的上传文件覆盖时,应先从数据库或物理存储上检查文件名,再上传文件;
- 先对上传的内容运行病毒/恶意软件扫描程序,然后再存储文件。
解决方案
存储方案
数据库存储:
- 对于小型文件上传,数据库通常快于物理存储;
- 数据库比物理存储更为方便,因为可以在查询用户数据时同事可以查询文件(例如用户头像)
- 本地数据库存储比云数据库存储成本要低
物理存储:
- 对于大型文件上传,数据库限制可能会限制上传的大小。
- 物理存储比数据库存储成本更高
- 进程需对存储位置有读写权限。切勿授予执行权限。
云数据存储服务
- 服务通常通过本地解决方案提供提升的可伸缩性和复原能力,而它们往往受单一故障点的影响
- 在大型存储基础结构方案中,服务的成本可能更低。
文件上传方案
缓冲
先将整个文件保存到内存,然后通过IFormFile得到stream。如果并发文件上传的数量和大小超过了内存的容量,网站就会因为内存不足而崩溃。
流式处理
将一个stream分多个Section读取并写入,无需将整个请求放入内存。流式传输无法显著提高性能。 流式传输可降低上传文件时对内存或磁盘空间的需求。
.net core 流式处理示例
完整代码:github代码路径
修改Kestrel最大请求正文限制
由于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<IActionResult> 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<string, List<byte[]>> _fileSignature = new Dictionary<string, List<byte[]>>
{
{ ".zip", new List<byte[]>
{
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<byte[]> { new byte[] { 0x52, 0x61, 0x72, 0x21, 0x1A, 0x07,0x00 } } },
{ ".7z", new List<byte[]>{ 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)
{
// 文件过大的处理...
}