Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Api/Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.6" />
<PackageReference Include="NewRelic.Agent" Version="8.30.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="5.5.1" />
<PackageReference Include="Microsoft.Azure.EventGrid" Version="3.2.0" />
</ItemGroup>

<ItemGroup>
Expand Down
114 changes: 94 additions & 20 deletions src/Api/Controllers/SendsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
using Bit.Core.Models.Api;
using Bit.Core.Exceptions;
using Bit.Core.Services;
using Bit.Api.Utilities;
using Bit.Core.Models.Table;
using Bit.Core.Utilities;
using Bit.Core.Settings;
using Bit.Core.Models.Api.Response;
using Bit.Core.Enums;
using Microsoft.Azure.EventGrid.Models;
using Bit.Api.Utilities;
using System.Collections.Generic;

namespace Bit.Api.Controllers
{
Expand Down Expand Up @@ -69,14 +71,40 @@ public async Task<IActionResult> Access(string id, [FromBody] SendAccessRequestM
}

[AllowAnonymous]
[HttpGet("access/file/{id}")]
public async Task<SendFileDownloadDataResponseModel> GetSendFileDownloadData(string id)
[HttpPost("{encodedSendId}/access/file/{fileId}")]
public async Task<IActionResult> GetSendFileDownloadData(string encodedSendId,
string fileId, [FromBody] SendAccessRequestModel model)
{
return new SendFileDownloadDataResponseModel()
var sendId = new Guid(CoreHelpers.Base64UrlDecode(encodedSendId));
var send = await _sendRepository.GetByIdAsync(sendId);

if (send == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also missing a check here as to whether the Send is enabled or has exceeded it's access count. Although not sure if the access count parameter gets another check here since it would have already been incremented by 1 before downloading the file (assuming).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sizable changes to fix this, but I think I hit the points we discussed. there's still a bit of a data leak in that you learn the name and size of the file without incrementing AccessCount, but for now that seems a small issue

{
Id = id,
Url = await _sendFileStorageService.GetSendFileDownloadUrlAsync(id),
};
throw new BadRequestException("Could not locate send");
}

var (url, passwordRequired, passwordInvalid) = await _sendService.GetSendFileDownloadUrlAsync(send, fileId,
model.Password);

if (passwordRequired)
{
return new UnauthorizedResult();
}
if (passwordInvalid)
{
await Task.Delay(2000);
throw new BadRequestException("Invalid password.");
}
if (send == null)
{
throw new NotFoundException();
}

return new ObjectResult(new SendFileDownloadDataResponseModel()
{
Id = fileId,
Url = url,
});
}

[HttpGet("{id}")]
Expand Down Expand Up @@ -112,31 +140,77 @@ public async Task<SendResponseModel> Post([FromBody] SendRequestModel model)
}

[HttpPost("file")]
[RequestSizeLimit(105_906_176)]
public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendRequestModel model)
{
if (model.Type != SendType.File)
{
throw new BadRequestException("Invalid content.");
}

if (!model.FileLength.HasValue)
{
throw new BadRequestException("Invalid content. File size hint is required.");
}

var userId = _userService.GetProperUserId(User).Value;
var (send, data) = model.ToSend(userId, model.File.FileName, _sendService);
var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value);
return new SendFileUploadDataResponseModel
{
Url = uploadUrl,
FileUploadType = _sendFileStorageService.FileUploadType,
SendResponse = new SendResponseModel(send, _globalSettings)
};
}

[HttpPost("{id}/file/{fileId}")]
[DisableFormValueModelBinding]
public async Task<SendResponseModel> PostFile()
public async Task PostFileForExistingSend(string id, string fileId)
{
if (!Request?.ContentType.Contains("multipart/") ?? true)
{
throw new BadRequestException("Invalid content.");
}

if (Request.ContentLength > 105906176) // 101 MB, give em' 1 extra MB for cushion
if (Request.ContentLength > 105906176 && !_globalSettings.SelfHosted) // 101 MB, give em' 1 extra MB for cushion
{
throw new BadRequestException("Max file size is 100 MB.");
throw new BadRequestException("Max file size for direct upload is 100 MB.");
}

Send send = null;
await Request.GetSendFileAsync(async (stream, fileName, model) =>
var send = await _sendRepository.GetByIdAsync(new Guid(id));
await Request.GetSendFileAsync(async (stream) =>
{
model.ValidateCreation();
var userId = _userService.GetProperUserId(User).Value;
var (madeSend, madeData) = model.ToSend(userId, fileName, _sendService);
send = madeSend;
await _sendService.CreateSendAsync(send, madeData, stream, Request.ContentLength.GetValueOrDefault(0));
await _sendService.UploadFileToExistingSendAsync(stream, send);
});
}

return new SendResponseModel(send, _globalSettings);
[AllowAnonymous]
[HttpPost("file/validate/azure")]
public async Task<OkObjectResult> AzureValidateFile()
{
return await ApiHelpers.HandleAzureEvents(Request, new Dictionary<string, Func<EventGridEvent, Task>>
{
{
"Microsoft.Storage.BlobCreated", async (eventGridEvent) =>
{
try
{
var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1];
var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName);
var send = await _sendRepository.GetByIdAsync(new Guid(sendId));
if (send == null)
{
return;
}
await _sendService.ValidateSendFile(send);
}
catch
{
return;
}
}
}
});
}

[HttpPut("{id}")]
Expand Down
47 changes: 47 additions & 0 deletions src/Api/Utilities/ApiHelpers.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.EventGrid;
using Microsoft.Azure.EventGrid.Models;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;

Expand Down Expand Up @@ -29,5 +34,47 @@ public async static Task<T> ReadJsonFileFromBody<T>(HttpContext httpContext, IFo

return obj;
}

/// <summary>
/// Validates Azure event subscription and calls the appropriate event handler. Responds HttpOk.
/// </summary>
/// <param name="request">HttpRequest received from Azure</param>
/// <param name="eventTypeHandlers">Dictionary of eventType strings and their associated handlers.</param>
/// <returns>OkObjectResult</returns>
/// <remarks>Reference https://docs.microsoft.com/en-us/azure/event-grid/receive-events</remarks>
public async static Task<OkObjectResult> HandleAzureEvents(HttpRequest request,
Dictionary<string, Func<EventGridEvent, Task>> eventTypeHandlers)
{
var response = string.Empty;
var requestContent = await new StreamReader(request.Body).ReadToEndAsync();
if (string.IsNullOrWhiteSpace(requestContent))
{
return new OkObjectResult(response);
}

var eventGridSubscriber = new EventGridSubscriber();
var eventGridEvents = eventGridSubscriber.DeserializeEventGridEvents(requestContent);

foreach (var eventGridEvent in eventGridEvents)
{
if (eventGridEvent.Data is SubscriptionValidationEventData eventData)
{
// Might want to enable additional validation: subject, topic etc.

var responseData = new SubscriptionValidationResponse()
{
ValidationResponse = eventData.ValidationCode
};

return new OkObjectResult(responseData);
}
else if (eventTypeHandlers.ContainsKey(eventGridEvent.EventType))
{
await eventTypeHandlers[eventGridEvent.EventType](eventGridEvent);
}
}

return new OkObjectResult(response);
}
}
}
37 changes: 8 additions & 29 deletions src/Api/Utilities/MultipartFormDataHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,45 +66,24 @@ public static async Task GetFileAsync(this HttpRequest request, Func<Stream, str
}
}

public static async Task GetSendFileAsync(this HttpRequest request, Func<Stream, string,
SendRequestModel, Task> callback)
public static async Task GetSendFileAsync(this HttpRequest request, Func<Stream, Task> callback)
{
var boundary = GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType),
_defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, request.Body);

var firstSection = await reader.ReadNextSectionAsync();
if (firstSection != null)
var dataSection = await reader.ReadNextSectionAsync();
if (dataSection != null)
{
if (ContentDispositionHeaderValue.TryParse(firstSection.ContentDisposition, out _))
if (ContentDispositionHeaderValue.TryParse(dataSection.ContentDisposition, out var dataContent)
&& HasFileContentDisposition(dataContent))
{
// Request model json, then data
string requestModelJson = null;
using (var sr = new StreamReader(firstSection.Body))
using (dataSection.Body)
{
requestModelJson = await sr.ReadToEndAsync();
await callback(dataSection.Body);
}

var secondSection = await reader.ReadNextSectionAsync();
if (secondSection != null)
{
if (ContentDispositionHeaderValue.TryParse(secondSection.ContentDisposition,
out var secondContent) && HasFileContentDisposition(secondContent))
{
var fileName = HeaderUtilities.RemoveQuotes(secondContent.FileName).ToString();
using (secondSection.Body)
{
var model = JsonConvert.DeserializeObject<SendRequestModel>(requestModelJson);
await callback(secondSection.Body, fileName, model);
}
}

secondSection = null;
}

}

firstSection = null;
dataSection = null;
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/Core/Enums/FileUploadType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Bit.Core.Enums
{
public enum FileUploadType
{
Direct = 0,
Azure = 1,
}
}
1 change: 1 addition & 0 deletions src/Core/Models/Api/Request/SendRequestModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ namespace Bit.Core.Models.Api
public class SendRequestModel
{
public SendType Type { get; set; }
public long? FileLength { get; set; } = null;
[EncryptedString]
[EncryptedStringLength(1000)]
public string Name { get; set; }
Expand Down
13 changes: 13 additions & 0 deletions src/Core/Models/Api/Response/SendFileUploadDataResponseModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Bit.Core.Enums;

namespace Bit.Core.Models.Api.Response
{
public class SendFileUploadDataResponseModel : ResponseModel
{
public string Url { get; set; }
public FileUploadType FileUploadType { get; set; }
public SendResponseModel SendResponse { get; set; }

public SendFileUploadDataResponseModel() : base("send-fileUpload") { }
}
}
5 changes: 4 additions & 1 deletion src/Core/Services/ISendService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ public interface ISendService
{
Task DeleteSendAsync(Send send);
Task SaveSendAsync(Send send);
Task CreateSendAsync(Send send, SendFileData data, Stream stream, long requestLength);
Task<string> SaveFileSendAsync(Send send, SendFileData data, long fileLength);
Task UploadFileToExistingSendAsync(Stream stream, Send send);
Task<(Send, bool, bool)> AccessAsync(Guid sendId, string password);
string HashPassword(string password);
Task<bool> ValidateSendFile(Send send);
Task<(string, bool, bool)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password);
}
}
10 changes: 7 additions & 3 deletions src/Core/Services/ISendStorageService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.Models.Table;
using Bit.Core.Enums;
using Bit.Core.Models.Table;
using System;
using System.IO;
using System.Threading.Tasks;
Expand All @@ -7,10 +8,13 @@ namespace Bit.Core.Services
{
public interface ISendFileStorageService
{
FileUploadType FileUploadType { get; }
Task UploadNewFileAsync(Stream stream, Send send, string fileId);
Task DeleteFileAsync(string fileId);
Task DeleteFileAsync(Send send, string fileId);
Task DeleteFilesForOrganizationAsync(Guid organizationId);
Task DeleteFilesForUserAsync(Guid userId);
Task<string> GetSendFileDownloadUrlAsync(string fileId);
Task<string> GetSendFileDownloadUrlAsync(Send send, string fileId);
Task<string> GetSendFileUploadUrlAsync(Send send, string fileId);
Task<bool> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway);
}
}
Loading