From 3e29a488fd7a9a8ee9cae4ad200934db5d47d365 Mon Sep 17 00:00:00 2001 From: Artem Dudarev Date: Wed, 11 Dec 2024 13:02:28 +0200 Subject: [PATCH] fix: Fix path traversal when uploading a module ZIP (#2867) --- .../Controllers/Api/ModulesController.cs | 148 ++++++++++++------ 1 file changed, 100 insertions(+), 48 deletions(-) diff --git a/src/VirtoCommerce.Platform.Web/Controllers/Api/ModulesController.cs b/src/VirtoCommerce.Platform.Web/Controllers/Api/ModulesController.cs index a93b0fdd32c..d8865e5f2d0 100644 --- a/src/VirtoCommerce.Platform.Web/Controllers/Api/ModulesController.cs +++ b/src/VirtoCommerce.Platform.Web/Controllers/Api/ModulesController.cs @@ -29,6 +29,8 @@ namespace VirtoCommerce.Platform.Web.Controllers.Api [Authorize] public class ModulesController : Controller { + private const string _managementIsDisabledMessage = "Module management is disabled."; + private readonly IExternalModuleCatalog _externalModuleCatalog; private readonly IModuleInstaller _moduleInstaller; private readonly IPushNotificationManager _pushNotifier; @@ -36,11 +38,21 @@ public class ModulesController : Controller private readonly ISettingsManager _settingsManager; private readonly PlatformOptions _platformOptions; private readonly ExternalModuleCatalogOptions _externalModuleCatalogOptions; + private readonly LocalStorageModuleCatalogOptions _localStorageModuleCatalogOptions; private readonly IPlatformRestarter _platformRestarter; private static readonly object _lockObject = new object(); private static readonly FormOptions _defaultFormOptions = new FormOptions(); - public ModulesController(IExternalModuleCatalog externalModuleCatalog, IModuleInstaller moduleInstaller, IPushNotificationManager pushNotifier, IUserNameResolver userNameResolver, ISettingsManager settingsManager, IOptions platformOptions, IOptions externalModuleCatalogOptions, IPlatformRestarter platformRestarter) + public ModulesController( + IExternalModuleCatalog externalModuleCatalog, + IModuleInstaller moduleInstaller, + IPushNotificationManager pushNotifier, + IUserNameResolver userNameResolver, + ISettingsManager settingsManager, + IOptions platformOptions, + IOptions externalModuleCatalogOptions, + IOptions localStorageModuleCatalogOptions, + IPlatformRestarter platformRestarter) { _externalModuleCatalog = externalModuleCatalog; _moduleInstaller = moduleInstaller; @@ -49,6 +61,7 @@ public ModulesController(IExternalModuleCatalog externalModuleCatalog, IModuleIn _settingsManager = settingsManager; _platformOptions = platformOptions.Value; _externalModuleCatalogOptions = externalModuleCatalogOptions.Value; + _localStorageModuleCatalogOptions = localStorageModuleCatalogOptions.Value; _platformRestarter = platformRestarter; } @@ -143,69 +156,108 @@ public ActionResult GetMissingDependencies([FromBody] Module public async Task> UploadModuleArchive() { EnsureModulesCatalogInitialized(); - ModuleDescriptor result = null; + + if (!_localStorageModuleCatalogOptions.RefreshProbingFolderOnStart) + { + return BadRequest(_managementIsDisabledMessage); + } + if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) { return BadRequest($"Expected a multipart request, but got {Request.ContentType}"); } - var uploadPath = Path.GetFullPath(_platformOptions.LocalUploadFolderPath); - if (!Directory.Exists(uploadPath)) + + var targetFilePath = await UploadFile(Request, Path.GetFullPath(_platformOptions.LocalUploadFolderPath)); + if (targetFilePath is null) + { + return BadRequest("Cannot read file"); + } + + var manifest = await LoadModuleManifestFromZipArchive(targetFilePath); + if (manifest is null) + { + return BadRequest("Cannot read module manifest"); + } + + var module = AbstractTypeFactory.TryCreateInstance(); + module.LoadFromManifest(manifest); + var existingModule = _externalModuleCatalog.Modules.OfType().FirstOrDefault(x => x.Equals(module)); + + if (existingModule != null) + { + module = existingModule; + } + else { - Directory.CreateDirectory(uploadPath); + //Force dependency validation for new module + _externalModuleCatalog.CompleteListWithDependencies([module]).ToList().Clear(); + _externalModuleCatalog.AddModule(module); } - string targetFilePath = null; - var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit); - var reader = new MultipartReader(boundary, HttpContext.Request.Body); + module.Ref = targetFilePath; + var result = new ModuleDescriptor(module); + + return Ok(result); + } + private static async Task UploadFile(HttpRequest request, string uploadFolderPath) + { + var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit); + var reader = new MultipartReader(boundary, request.Body); var section = await reader.ReadNextSectionAsync(); - if (section != null) + + if (section == null) { - var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition); + return null; + } - if (hasContentDispositionHeader) - { - if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) - { - var fileName = contentDisposition.FileName.Value; - targetFilePath = Path.Combine(uploadPath, fileName); + if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition) || + !MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) + { + return null; + } - using (var targetStream = System.IO.File.Create(targetFilePath)) - { - await section.Body.CopyToAsync(targetStream); - } + var fileName = Path.GetFileName(contentDisposition.FileName.Value); + if (string.IsNullOrEmpty(fileName)) + { + return null; + } - } - } - using (var packageStream = System.IO.File.Open(targetFilePath, FileMode.Open)) - using (var package = new ZipArchive(packageStream, ZipArchiveMode.Read)) + if (!Directory.Exists(uploadFolderPath)) + { + Directory.CreateDirectory(uploadFolderPath); + } + + var targetFilePath = Path.Combine(uploadFolderPath, fileName); + + await using var targetStream = System.IO.File.Create(targetFilePath); + await section.Body.CopyToAsync(targetStream); + + return targetFilePath; + } + + private static async Task LoadModuleManifestFromZipArchive(string path) + { + ModuleManifest manifest = null; + + try + { + await using var packageStream = System.IO.File.Open(path, FileMode.Open); + using var package = new ZipArchive(packageStream, ZipArchiveMode.Read); + + var entry = package.GetEntry("module.manifest"); + if (entry != null) { - var entry = package.GetEntry("module.manifest"); - if (entry != null) - { - using (var manifestStream = entry.Open()) - { - var manifest = ManifestReader.Read(manifestStream); - var module = AbstractTypeFactory.TryCreateInstance(); - module.LoadFromManifest(manifest); - var alreadyExistModule = _externalModuleCatalog.Modules.OfType().FirstOrDefault(x => x.Equals(module)); - if (alreadyExistModule != null) - { - module = alreadyExistModule; - } - else - { - //Force dependency validation for new module - _externalModuleCatalog.CompleteListWithDependencies(new[] { module }).ToList().Clear(); - _externalModuleCatalog.AddModule(module); - } - module.Ref = targetFilePath; - result = new ModuleDescriptor(module); - } - } + await using var manifestStream = entry.Open(); + manifest = ManifestReader.Read(manifestStream); } } - return Ok(result); + catch + { + // Suppress any exceptions + } + + return manifest; } ///