mirror of
https://github.com/bitwarden/server.git
synced 2025-07-17 07:30:59 -05:00
[PM-22610] validate file within max length; log deletion of invalid uploads (#5960)
This commit is contained in:
@ -8,6 +8,7 @@ using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Tools.SendFeatures.Commands;
|
||||
|
||||
@ -18,19 +19,22 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly ISendValidationService _sendValidationService;
|
||||
private readonly ISendCoreHelperService _sendCoreHelperService;
|
||||
private readonly ILogger<NonAnonymousSendCommand> _logger;
|
||||
|
||||
public NonAnonymousSendCommand(ISendRepository sendRepository,
|
||||
ISendFileStorageService sendFileStorageService,
|
||||
IPushNotificationService pushNotificationService,
|
||||
ISendAuthorizationService sendAuthorizationService,
|
||||
ISendValidationService sendValidationService,
|
||||
ISendCoreHelperService sendCoreHelperService)
|
||||
ISendCoreHelperService sendCoreHelperService,
|
||||
ILogger<NonAnonymousSendCommand> logger)
|
||||
{
|
||||
_sendRepository = sendRepository;
|
||||
_sendFileStorageService = sendFileStorageService;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_sendValidationService = sendValidationService;
|
||||
_sendCoreHelperService = sendCoreHelperService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SaveSendAsync(Send send)
|
||||
@ -63,6 +67,11 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
||||
throw new BadRequestException("No file data.");
|
||||
}
|
||||
|
||||
if (fileLength > SendFileSettingHelper.MAX_FILE_SIZE)
|
||||
{
|
||||
throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}.");
|
||||
}
|
||||
|
||||
var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send);
|
||||
|
||||
if (storageBytesRemaining < fileLength)
|
||||
@ -77,13 +86,17 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
||||
data.Id = fileId;
|
||||
data.Size = fileLength;
|
||||
data.Validated = false;
|
||||
send.Data = JsonSerializer.Serialize(data,
|
||||
JsonHelpers.IgnoreWritingNull);
|
||||
send.Data = JsonSerializer.Serialize(data, JsonHelpers.IgnoreWritingNull);
|
||||
await SaveSendAsync(send);
|
||||
return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Deleted file from {SendId} because an error occurred when creating the upload URL.",
|
||||
send.Id
|
||||
);
|
||||
|
||||
// Clean up since this is not transactional
|
||||
await _sendFileStorageService.DeleteFileAsync(send, fileId);
|
||||
throw;
|
||||
@ -135,23 +148,31 @@ public class NonAnonymousSendCommand : INonAnonymousSendCommand
|
||||
{
|
||||
var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);
|
||||
|
||||
var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY);
|
||||
var minimum = fileData.Size - SendFileSettingHelper.FILE_SIZE_LEEWAY;
|
||||
var maximum = Math.Min(
|
||||
fileData.Size + SendFileSettingHelper.FILE_SIZE_LEEWAY,
|
||||
SendFileSettingHelper.MAX_FILE_SIZE
|
||||
);
|
||||
var (valid, size) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, minimum, maximum);
|
||||
|
||||
if (!valid || realSize > SendFileSettingHelper.FILE_SIZE_LEEWAY)
|
||||
// protect file service from upload hijacking by deleting invalid sends
|
||||
if (!valid)
|
||||
{
|
||||
// File reported differs in size from that promised. Must be a rogue client. Delete Send
|
||||
_logger.LogWarning(
|
||||
"Deleted {SendId} because its reported size {Size} was outside the expected range ({Minimum} - {Maximum}).",
|
||||
send.Id,
|
||||
size,
|
||||
minimum,
|
||||
maximum
|
||||
);
|
||||
await DeleteSendAsync(send);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update Send data if necessary
|
||||
if (realSize != fileData.Size)
|
||||
{
|
||||
fileData.Size = realSize.Value;
|
||||
}
|
||||
// replace expected size with validated size
|
||||
fileData.Size = size;
|
||||
fileData.Validated = true;
|
||||
send.Data = JsonSerializer.Serialize(fileData,
|
||||
JsonHelpers.IgnoreWritingNull);
|
||||
send.Data = JsonSerializer.Serialize(fileData, JsonHelpers.IgnoreWritingNull);
|
||||
await SaveSendAsync(send);
|
||||
|
||||
return valid;
|
||||
|
@ -88,7 +88,7 @@ public class AzureSendFileStorageService : ISendFileStorageService
|
||||
return sasUri.ToString();
|
||||
}
|
||||
|
||||
public async Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway)
|
||||
public async Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum)
|
||||
{
|
||||
await InitAsync();
|
||||
|
||||
@ -116,17 +116,14 @@ public class AzureSendFileStorageService : ISendFileStorageService
|
||||
await blobClient.SetHttpHeadersAsync(headers);
|
||||
|
||||
var length = blobProperties.Value.ContentLength;
|
||||
if (length < expectedFileSize - leeway || length > expectedFileSize + leeway)
|
||||
{
|
||||
return (false, length);
|
||||
}
|
||||
var valid = minimum <= length || length <= maximum;
|
||||
|
||||
return (true, length);
|
||||
return (valid, length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled error in ValidateFileAsync");
|
||||
return (false, null);
|
||||
_logger.LogError(ex, $"A storage operation failed in {nameof(ValidateFileAsync)}");
|
||||
return (false, -1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,16 +56,13 @@ public interface ISendFileStorageService
|
||||
/// </summary>
|
||||
/// <param name="send"><see cref="Send" /> used to help validate file</param>
|
||||
/// <param name="fileId">File id to identify which file to validate</param>
|
||||
/// <param name="expectedFileSize">Expected file size of the file</param>
|
||||
/// <param name="leeway">
|
||||
/// Send file size tolerance in bytes. When an uploaded file's `expectedFileSize`
|
||||
/// is outside of the leeway, the storage operation fails.
|
||||
/// </param>
|
||||
/// <throws>
|
||||
/// ❌ Fill this in with an explanation of the error thrown when `leeway` is incorrect
|
||||
/// </throws>
|
||||
/// <returns>Task object for async operations with Tuple of boolean that determines if file was valid and long that
|
||||
/// the actual file size of the file.
|
||||
/// <param name="minimum">The minimum allowed length of the stored file in bytes.</param>
|
||||
/// <param name="maximum">The maximuim allowed length of the stored file in bytes</param>
|
||||
/// <returns>
|
||||
/// A task that completes when validation is finished. The first element of the tuple is
|
||||
/// <see langword="true" /> when validation succeeded, and false otherwise. The second element
|
||||
/// of the tuple contains the observed file length in bytes. If an error occurs during validation,
|
||||
/// this returns `-1`.
|
||||
/// </returns>
|
||||
Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway);
|
||||
Task<(bool valid, long length)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum);
|
||||
}
|
||||
|
@ -85,9 +85,9 @@ public class LocalSendStorageService : ISendFileStorageService
|
||||
public Task<string> GetSendFileUploadUrlAsync(Send send, string fileId)
|
||||
=> Task.FromResult($"/sends/{send.Id}/file/{fileId}");
|
||||
|
||||
public Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway)
|
||||
public Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum)
|
||||
{
|
||||
long? length = null;
|
||||
long length = -1;
|
||||
var path = FilePath(send, fileId);
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
@ -95,11 +95,7 @@ public class LocalSendStorageService : ISendFileStorageService
|
||||
}
|
||||
|
||||
length = new FileInfo(path).Length;
|
||||
if (expectedFileSize < length - leeway || expectedFileSize > length + leeway)
|
||||
{
|
||||
return Task.FromResult((false, length));
|
||||
}
|
||||
|
||||
return Task.FromResult((true, length));
|
||||
var valid = minimum < length || length < maximum;
|
||||
return Task.FromResult((valid, length));
|
||||
}
|
||||
}
|
||||
|
@ -37,8 +37,8 @@ public class NoopSendFileStorageService : ISendFileStorageService
|
||||
return Task.FromResult((string)null);
|
||||
}
|
||||
|
||||
public Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway)
|
||||
public Task<(bool, long)> ValidateFileAsync(Send send, string fileId, long minimum, long maximum)
|
||||
{
|
||||
return Task.FromResult((false, default(long?)));
|
||||
return Task.FromResult((false, -1L));
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user