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 <[email protected]>
  • Loading branch information
BenedekFarkas and LombiqTechnologies authored 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 @@ private static string ConvertToRelativeUriPath(string path) {
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 IEnumerable<IStorageFile> ListFiles(string path) {
}

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 bool TryCreateFolder(string path) {

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 void RenameFolder(string path, string newPath) {
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 void RenameFile(string path, string newPath) {
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 void CopyFile(string path, string newPath) {
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 @@ public AzureBlobFolderStorage(CloudBlobDirectory blob, string rootPath) {
_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 static long GetDirectorySize(CloudBlobDirectory directoryBlob) {
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 ActionResult Upload(string folderPath, string type) {
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 ActionResult Replace(int replaceId, string type) {
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 ActionResult Replace(int replaceId, string type) {
});
}
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 @@ public ActionResult Create() {
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 @@ public ActionResult Edit() {
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 @@ public ActionResult Edit() {
}

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 @@ public ActionResult Move(string folderPath, int[] mediaItemIds) {
// 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 @@ protected override DriverResult Editor(MediaPart part, IUpdateModel updater, dyn
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 MediaPart ImportMedia(Stream stream, string relativePath, string filename
}

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 MediaPart ImportMedia(string relativePath, string filename, string conten
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 bool CheckMediaFolderPermission(Orchard.Security.Permissions.Permission p
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 bool CheckMediaFolderPermission(Orchard.Security.Permissions.Permission p
isMyfolder = true;
}

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

0 comments on commit 0b86413

Please sign in to comment.