Skip to content

Commit

Permalink
6748: Stricter file and folder name validation (#6792)
Browse files Browse the repository at this point in the history
* Media Library: More strict file and folder name validation, fixes #6748

* Resetting MediaLibraryService changes to 1.10.x

* Code styling in FileSystemStorageProvider

* Adding string file and folder name validation to FileSystemStorageProvider, so that MediaLibrary components don't need to do it separately

* Applying the same file and folder name validation to AzureFileSystem too

* Code styling and fixes in AzureFileSystem, MediaLibrary and IStorageProvider

* Simplifying invalid character detection

* Code styling

* Adding InvalidNameCharacterException to be able to handle invalid characters precisely at various user-facing components

* Updating MediaLibrary not to log an error when a file can't be uploaded due to invalid characters

---------

Co-authored-by: Lombiq <github@lombiq.com>
  • Loading branch information
BenedekFarkas and LombiqTechnologies committed Apr 18, 2024
1 parent 3a6810e commit 0b86413
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 91 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ public class AzureFileSystem {
return newPath;
}

private static string GetFolderName(string path) => path.Substring(path.LastIndexOf('/') + 1);

public string Combine(string path1, string path2) {
if (path1 == null) {
throw new ArgumentNullException("path1");
Expand Down Expand Up @@ -141,10 +143,10 @@ public class AzureFileSystem {
}

return BlobClient.ListBlobs(prefix)
.OfType<CloudBlockBlob>()
.Where(blobItem => !blobItem.Uri.AbsoluteUri.EndsWith(FolderEntry))
.Select(blobItem => new AzureBlobFileStorage(blobItem, _absoluteRoot))
.ToArray();
.OfType<CloudBlockBlob>()
.Where(blobItem => !blobItem.Uri.AbsoluteUri.EndsWith(FolderEntry))
.Select(blobItem => new AzureBlobFileStorage(blobItem, _absoluteRoot))
.ToArray();
}

public IEnumerable<IStorageFolder> ListFolders(string path) {
Expand Down Expand Up @@ -194,6 +196,11 @@ public class AzureFileSystem {

public void CreateFolder(string path) {
path = ConvertToRelativeUriPath(path);

if (FileSystemStorageProvider.FolderNameContainsInvalidCharacters(GetFolderName(path))) {
throw new InvalidNameCharacterException("The directory name contains invalid character(s)");
}

Container.EnsureDirectoryDoesNotExist(String.Concat(_root, path));

// Creating a virtually hidden file to make the directory an existing concept
Expand Down Expand Up @@ -225,6 +232,10 @@ public class AzureFileSystem {
path = ConvertToRelativeUriPath(path);
newPath = ConvertToRelativeUriPath(newPath);

if (FileSystemStorageProvider.FolderNameContainsInvalidCharacters(GetFolderName(newPath))) {
throw new InvalidNameCharacterException("The new directory name contains invalid character(s)");
}

if (!path.EndsWith("/"))
path += "/";

Expand Down Expand Up @@ -260,6 +271,10 @@ public class AzureFileSystem {
path = ConvertToRelativeUriPath(path);
newPath = ConvertToRelativeUriPath(newPath);

if (FileSystemStorageProvider.FileNameContainsInvalidCharacters(Path.GetFileName(newPath))) {
throw new InvalidNameCharacterException("The new file name contains invalid character(s)");
}

Container.EnsureBlobExists(String.Concat(_root, path));
Container.EnsureBlobDoesNotExist(String.Concat(_root, newPath));

Expand All @@ -284,6 +299,10 @@ public class AzureFileSystem {
public IStorageFile CreateFile(string path) {
path = ConvertToRelativeUriPath(path);

if (FileSystemStorageProvider.FileNameContainsInvalidCharacters(Path.GetFileName(path))) {
throw new InvalidNameCharacterException("The file name contains invalid character(s)");
}

if (Container.BlobExists(String.Concat(_root, path))) {
throw new ArgumentException("File " + path + " already exists");
}
Expand Down Expand Up @@ -371,10 +390,7 @@ private class AzureBlobFolderStorage : IStorageFolder {
_rootPath = rootPath;
}

public string GetName() {
var path = GetPath();
return path.Substring(path.LastIndexOf('/') + 1);
}
public string GetName() => GetFolderName(GetPath());

public string GetPath() {
return _blob.Uri.ToString().Substring(_rootPath.Length).Trim('/');
Expand All @@ -399,11 +415,12 @@ private class AzureBlobFolderStorage : IStorageFolder {
long size = 0;

foreach (var blobItem in directoryBlob.ListBlobs()) {
if (blobItem is CloudBlockBlob)
size += ((CloudBlockBlob)blobItem).Properties.Length;

if (blobItem is CloudBlobDirectory)
size += GetDirectorySize((CloudBlobDirectory)blobItem);
if (blobItem is CloudBlockBlob blob) {
size += blob.Properties.Length;
}
else if (blobItem is CloudBlobDirectory directory) {
size += GetDirectorySize(directory);
}
}

return size;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
using System.IO;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.FileSystems.Media;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.MediaLibrary.Models;
using Orchard.MediaLibrary.Services;
using Orchard.MediaLibrary.ViewModels;
using Orchard.Themes;
using Orchard.UI.Admin;
using Orchard.MediaLibrary.Models;
using Orchard.Localization;
using System.Linq;
using Orchard.FileSystems.Media;
using Orchard.Logging;

namespace Orchard.MediaLibrary.Controllers {
[Admin, Themed(false)]
Expand Down Expand Up @@ -107,10 +106,16 @@ public class ClientStorageController : Controller {
url = mediaPart.FileName,
});
}
catch (InvalidNameCharacterException) {
statuses.Add(new {
error = T("The file name contains invalid character(s)").Text,
progress = 1.0,
});
}
catch (Exception ex) {
Logger.Error(ex, "Unexpected exception when uploading a media.");
Logger.Error(ex, T("Unexpected exception when uploading a media.").Text);
statuses.Add(new {
error = T(ex.Message).Text,
error = ex.Message,
progress = 1.0,
});
}
Expand All @@ -130,23 +135,24 @@ public class ClientStorageController : Controller {
return HttpNotFound();

// Check permission
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, replaceMedia.FolderPath) && _mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, replaceMedia.FolderPath))
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, replaceMedia.FolderPath) && _mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, replaceMedia.FolderPath))
&& !_mediaLibraryService.CanManageMediaFolder(replaceMedia.FolderPath)) {
return new HttpUnauthorizedResult();
}

var statuses = new List<object>();

var settings = Services.WorkContext.CurrentSite.As<MediaLibrarySettingsPart>();

// Loop through each file in the request
for (int i = 0; i < HttpContext.Request.Files.Count; i++) {
// Pointer to file
var file = HttpContext.Request.Files[i];
var filename = Path.GetFileName(file.FileName);

// if the file has been pasted, provide a default name
if (file.ContentType.Equals("image/png", StringComparison.InvariantCultureIgnoreCase) && !filename.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) {
if (file.ContentType.Equals("image/png", StringComparison.InvariantCultureIgnoreCase)
&& !filename.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase)) {
filename = "clipboard.png";
}

Expand Down Expand Up @@ -184,7 +190,7 @@ public class ClientStorageController : Controller {
});
}
catch (Exception ex) {
Logger.Error(ex, "Unexpected exception when uploading a media.");
Logger.Error(ex, T("Unexpected exception when uploading a media.").Text);

statuses.Add(new {
error = T(ex.Message).Text,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
using System;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Web.Mvc;
using Orchard.ContentManagement;
using Orchard.FileSystems.Media;
using Orchard.Localization;
using Orchard.Logging;
using Orchard.MediaLibrary.Models;
Expand Down Expand Up @@ -36,7 +36,7 @@ IMediaLibraryService mediaManagerService
public ActionResult Create(string folderPath) {
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, folderPath) || _mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, folderPath))) {
Services.Notifier.Error(T("Couldn't create media folder"));
return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath = folderPath });
return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath });
}

// If the user is trying to access a folder above his boundaries, redirect him to his home folder
Expand Down Expand Up @@ -68,28 +68,32 @@ IMediaLibraryService mediaManagerService
return new HttpUnauthorizedResult();
}

var failed = false;
try {
bool valid = String.IsNullOrWhiteSpace(viewModel.Name) || Regex.IsMatch(viewModel.Name, @"^[^:?#\[\]@!$&'()*+,.;=\s\""\<\>\\\|%]+$");
if (!valid) {
throw new ArgumentException(T("Folder contains invalid characters").ToString());
}
else {
_mediaLibraryService.CreateFolder(viewModel.FolderPath, viewModel.Name);
Services.Notifier.Information(T("Media folder created"));
}
_mediaLibraryService.CreateFolder(viewModel.FolderPath, viewModel.Name);
Services.Notifier.Information(T("Media folder created"));
}
catch (InvalidNameCharacterException) {
Services.Notifier.Error(T("The folder name contains invalid character(s)."));
failed = true;
}
catch (ArgumentException argumentException) {
Services.Notifier.Error(T("Creating Folder failed: {0}", argumentException.Message));
failed = true;
}

if (failed) {
Services.TransactionManager.Cancel();
return View(viewModel);
}

return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary" });
}

public ActionResult Edit(string folderPath) {
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, folderPath) || _mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, folderPath))) {
Services.Notifier.Error(T("Couldn't edit media folder"));
return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath = folderPath });
return RedirectToAction("Index", "Admin", new { area = "Orchard.MediaLibrary", folderPath });
}

if (!_mediaLibraryService.CanManageMediaFolder(folderPath)) {
Expand Down Expand Up @@ -125,7 +129,7 @@ IMediaLibraryService mediaManagerService
var viewModel = new MediaManagerFolderEditViewModel();
UpdateModel(viewModel);

if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, viewModel.FolderPath)
if (!(_mediaLibraryService.CheckMediaFolderPermission(Permissions.ImportMediaContent, viewModel.FolderPath)
|| _mediaLibraryService.CheckMediaFolderPermission(Permissions.EditMediaContent, viewModel.FolderPath))) {
return new HttpUnauthorizedResult();
}
Expand All @@ -136,14 +140,12 @@ IMediaLibraryService mediaManagerService
}

try {
bool valid = String.IsNullOrWhiteSpace(viewModel.Name) || Regex.IsMatch(viewModel.Name, @"^[^:?#\[\]@!$&'()*+,.;=\s\""\<\>\\\|%]+$");
if (!valid) {
throw new ArgumentException(T("Folder contains invalid characters").ToString());
}
else {
_mediaLibraryService.RenameFolder(viewModel.FolderPath, viewModel.Name);
Services.Notifier.Information(T("Media folder renamed"));
}
_mediaLibraryService.RenameFolder(viewModel.FolderPath, viewModel.Name);
Services.Notifier.Information(T("Media folder renamed"));
}
catch (InvalidNameCharacterException) {
Services.Notifier.Error(T("The folder name contains invalid character(s)."));
return View(viewModel);
}
catch (Exception exception) {
Services.Notifier.Error(T("Editing Folder failed: {0}", exception.Message));
Expand Down Expand Up @@ -198,7 +200,7 @@ IMediaLibraryService mediaManagerService
// don't try to rename the file if there is no associated media file
if (!string.IsNullOrEmpty(media.FileName)) {
// check permission on source folder
if(!_mediaLibraryService.CheckMediaFolderPermission(Permissions.DeleteMediaContent, media.FolderPath)) {
if (!_mediaLibraryService.CheckMediaFolderPermission(Permissions.DeleteMediaContent, media.FolderPath)) {
return new HttpUnauthorizedResult();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
using System;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.FileSystems.Media;
using Orchard.Localization;
using Orchard.MediaLibrary.Models;
using Orchard.MediaLibrary.Services;
using Orchard.Security;
using Orchard.UI.Notify;

namespace Orchard.MediaLibrary.MediaFileName
{
namespace Orchard.MediaLibrary.MediaFileName {
public class MediaFileNameDriver : ContentPartDriver<MediaPart> {
private readonly IAuthenticationService _authenticationService;
private readonly IAuthorizationService _authorizationService;
Expand Down Expand Up @@ -58,21 +58,27 @@ public class MediaFileNameDriver : ContentPartDriver<MediaPart> {
var priorFileName = model.FileName;
if (updater.TryUpdateModel(model, Prefix, null, null)) {
if (model.FileName != null && !model.FileName.Equals(priorFileName, StringComparison.OrdinalIgnoreCase)) {
var fieldName = "MediaFileNameEditorSettings.FileName";
try {
_mediaLibraryService.RenameFile(part.FolderPath, priorFileName, model.FileName);
part.FileName = model.FileName;
_notifier.Add(NotifyType.Information, T("File '{0}' was renamed to '{1}'", priorFileName, model.FileName));
}
catch (OrchardException) {
updater.AddModelError("MediaFileNameEditorSettings.FileName", T("Unable to rename file. Invalid Windows file path."));
updater.AddModelError(fieldName, T("Unable to rename file. Invalid Windows file path."));
}
catch (InvalidNameCharacterException) {
updater.AddModelError(fieldName, T("The file name contains invalid character(s)."));
}
catch (Exception) {
updater.AddModelError("MediaFileNameEditorSettings.FileName", T("Unable to rename file"));
catch (Exception exception) {
updater.AddModelError(fieldName, T("Unable to rename file: {0}", exception.Message));
}
}
}
}
return model;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.Core.Common.Models;
using Orchard.Core.Title.Models;
using Orchard.FileSystems.Media;
using Orchard.Localization;
using Orchard.MediaLibrary.Factories;
using Orchard.MediaLibrary.Models;
using Orchard.Core.Title.Models;
using Orchard.Validation;
using Orchard.MediaLibrary.Providers;
using Orchard.Validation;

namespace Orchard.MediaLibrary.Services {
public class MediaLibraryService : IMediaLibraryService {
Expand All @@ -21,7 +21,6 @@ public class MediaLibraryService : IMediaLibraryService {
private readonly IStorageProvider _storageProvider;
private readonly IEnumerable<IMediaFactorySelector> _mediaFactorySelectors;
private readonly IMediaFolderProvider _mediaFolderProvider;
private static char[] HttpUnallowed = new char[] { '<', '>', '*', '%', '&', ':', '\\', '?', '#' };

public MediaLibraryService(
IOrchardServices orchardServices,
Expand Down Expand Up @@ -145,12 +144,6 @@ public class MediaLibraryService : IMediaLibraryService {
}

public string GetUniqueFilename(string folderPath, string filename) {

// remove any char which is unallowed in an HTTP request
foreach (var unallowedChar in HttpUnallowed) {
filename = filename.Replace(unallowedChar.ToString(), "");
}

// compute a unique filename
var uniqueFilename = filename;
var index = 1;
Expand All @@ -177,9 +170,9 @@ public class MediaLibraryService : IMediaLibraryService {
var mediaFile = BuildMediaFile(relativePath, storageFile);

using (var stream = storageFile.OpenRead()) {
var mediaFactory = GetMediaFactory(stream, mimeType, contentType);
if (mediaFactory == null)
throw new Exception(T("No media factory available to handle this resource.").Text);
var mediaFactory = GetMediaFactory(stream, mimeType, contentType)
?? throw new Exception(T("No media factory available to handle this resource.").Text);

var mediaPart = mediaFactory.CreateMedia(stream, mediaFile.Name, mimeType, contentType);
if (mediaPart != null) {
mediaPart.FolderPath = relativePath;
Expand Down Expand Up @@ -256,7 +249,7 @@ public class MediaLibraryService : IMediaLibraryService {
if (_orchardServices.Authorizer.Authorize(Permissions.ManageMediaContent)) {
return true;
}
if (_orchardServices.WorkContext.CurrentUser==null)
if (_orchardServices.WorkContext.CurrentUser == null)
return _orchardServices.Authorizer.Authorize(permission);
// determines the folder type: public, user own folder (my), folder of another user (private)
var rootedFolderPath = this.GetRootedFolderPath(folderPath) ?? "";
Expand All @@ -268,7 +261,7 @@ public class MediaLibraryService : IMediaLibraryService {
isMyfolder = true;
}

if(isMyfolder) {
if (isMyfolder) {
return _orchardServices.Authorizer.Authorize(Permissions.ManageOwnMedia);
}
else { // other
Expand Down

0 comments on commit 0b86413

Please sign in to comment.