diff --git a/src/Spe/App_Config/Include/Spe/Spe.config b/src/Spe/App_Config/Include/Spe/Spe.config index 61f74505..7e751b7c 100644 --- a/src/Spe/App_Config/Include/Spe/Spe.config +++ b/src/Spe/App_Config/Include/Spe/Spe.config @@ -240,6 +240,15 @@ + + + + image/* + + + + + diff --git a/src/Spe/Client/Applications/UploadFile/PowerShellUploadFilePage2.cs b/src/Spe/Client/Applications/UploadFile/PowerShellUploadFilePage2.cs index 8621f09a..283443b0 100644 --- a/src/Spe/Client/Applications/UploadFile/PowerShellUploadFilePage2.cs +++ b/src/Spe/Client/Applications/UploadFile/PowerShellUploadFilePage2.cs @@ -9,6 +9,7 @@ using Sitecore.Shell.Web.UI; using Sitecore.Web; using Sitecore.Web.UI.XmlControls; +using Spe.Client.Applications.UploadFile.Validation; using Spe.Core.Diagnostics; namespace Spe.Client.Applications.UploadFile @@ -39,6 +40,16 @@ protected override void OnLoad(EventArgs e) return; try { + string[] patterns = Factory.GetStringSet("powershell/uploadFile/allowedFileTypes/pattern")?.ToArray() ?? new string[] { "image/*" }; + var contentTypeValidator = new ContentTypeValidator(patterns); + var result = contentTypeValidator.Validate(Request.Files); + if (!result.Valid) + { + CancelResult(); + Sitecore.Diagnostics.Log.Warn($"[SPE] {result.Message}", this); + return; + } + var pathOrId = Sitecore.Context.ClientPage.ClientRequest.Form["ItemUri"]; var langStr = Sitecore.Context.ClientPage.ClientRequest.Form["LanguageName"]; var language = langStr.Length > 0 @@ -57,6 +68,15 @@ protected override void OnLoad(EventArgs e) { uploadArgs.Destination = UploadDestination.File; uploadArgs.FileOnly = true; + string[] allowedLocations = Factory.GetStringSet("powershell/uploadFile/allowedLocations/path").ToArray(); + var validator = new UploadLocationValidator(allowedLocations); + pathOrId = validator.GetFullPath(pathOrId); + if (!validator.Validate(pathOrId)) + { + CancelResult(); + Sitecore.Diagnostics.Log.Warn($"[SPE] Location: '{pathOrId}' is protected. Please configure 'powershell/uploadFile/allowedLocations' if you wish to change it.", this); + return; + } } uploadArgs.Files = Request.Files; uploadArgs.Folder = pathOrId; @@ -125,5 +145,10 @@ protected override void OnLoad(EventArgs e) } } } + + private static void CancelResult() + { + HttpContext.Current.Response.Write("Done"); + } } } \ No newline at end of file diff --git a/src/Spe/Client/Applications/UploadFile/Validation/ContentTypeValidator.cs b/src/Spe/Client/Applications/UploadFile/Validation/ContentTypeValidator.cs new file mode 100644 index 00000000..804681cf --- /dev/null +++ b/src/Spe/Client/Applications/UploadFile/Validation/ContentTypeValidator.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Web; +using Sitecore; +using Sitecore.Diagnostics; +using Sitecore.Globalization; +using Sitecore.Pipelines.Upload; +using Sitecore.StringExtensions; + +namespace Spe.Client.Applications.UploadFile.Validation +{ + internal class ContentTypeValidator + { + internal IReadOnlyCollection validators { get; } + + public ContentTypeValidator(string[] patterns) + { + validators = CreateValidators(patterns); + } + + public ValidationResult Validate(HttpFileCollection Files) + { + if (!validators.Any()) + { + return new ValidationResult { Message = string.Empty, Valid = true }; + } + + foreach (string key in Files) + { + var file = Files[key]; + + if (file == null) + { + continue; + } + + if (!IsFileAccepted(file, validators)) + { + var reason = Translate.Text("File type isn`t allowed."); + reason = StringUtil.EscapeJavascriptString(reason); + var convertedFileName = StringUtil.EscapeJavascriptString(file.FileName); + + var errorText = Translate.Text(string.Format("The '{0}' file cannot be uploaded. File type isn`t allowed.", file.FileName)); + Log.Warn(errorText, this); + return new ValidationResult { Message = errorText, Valid = false }; + } + } + return new ValidationResult { Message = string.Empty, Valid = true }; + } + + protected static bool IsUnpack(HttpPostedFileBase file) + { + return string.Compare(Path.GetExtension(file.FileName), ".zip", StringComparison.InvariantCultureIgnoreCase) == 0; + } + + private static bool IsFileAccepted(HttpPostedFile file, IReadOnlyCollection validators) + { + if (string.IsNullOrEmpty(file.FileName)) + { + return true; + } + + var isArchive = IsUnpack(new HttpPostedFileWrapper(file)); + if (!isArchive) + { + return validators.Any(x => x.IsValid(file.FileName)); + } + + + if (file.InputStream.Position != 0) + { + file.InputStream.Position = 0; + } + + var archive = new ZipArchive(file.InputStream, ZipArchiveMode.Read, true); + try + { + return archive.Entries + .Where(entry => !entry.FullName.EndsWith("/")) + .All(entry => validators.Any(x => x.IsValid(entry.FullName))); + } + finally + { + archive.Dispose(); + if (file.InputStream.Position != 0) + { + file.InputStream.Position = 0; + } + } + } + + private static IReadOnlyCollection CreateValidators(string[] allowedFileTypes) + { + if (!allowedFileTypes.Any()) + { + return new List(); + } + + return allowedFileTypes + .Select(p => p.Trim()) + .Select(p => new FileTypeValidator(p)) + .ToList(); + } + } +} diff --git a/src/Spe/Client/Applications/UploadFile/Validation/FileTypeValidator.cs b/src/Spe/Client/Applications/UploadFile/Validation/FileTypeValidator.cs new file mode 100644 index 00000000..fa738c7d --- /dev/null +++ b/src/Spe/Client/Applications/UploadFile/Validation/FileTypeValidator.cs @@ -0,0 +1,36 @@ +using System; +using System.Linq; +using System.Web; + +namespace Spe.Client.Applications.UploadFile.Validation +{ + internal class FileTypeValidator + { + private readonly Func _validate; + + public FileTypeValidator(string pattern) + { + if (pattern.Contains('.')) + { + _validate = fileName => fileName.EndsWith(pattern); + } + else if (pattern.Contains("/*")) + { + _validate = fileName => MimeMapping.GetMimeMapping(fileName).Split('/').FirstOrDefault() == pattern.Split('/').First(); + } + else if (pattern.Contains("/")) + { + _validate = fileName => MimeMapping.GetMimeMapping(fileName) == pattern; + } + else + { + throw new NotSupportedException("Pattern isn't supported"); + } + } + + public bool IsValid(string fileName) + { + return _validate(fileName); + } + } +} diff --git a/src/Spe/Client/Applications/UploadFile/Validation/UploadLocationValidator.cs b/src/Spe/Client/Applications/UploadFile/Validation/UploadLocationValidator.cs new file mode 100644 index 00000000..2e4c2360 --- /dev/null +++ b/src/Spe/Client/Applications/UploadFile/Validation/UploadLocationValidator.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Web; +using Sitecore; +using Sitecore.Diagnostics; +using Sitecore.Globalization; +using Sitecore.Pipelines.Upload; +using Sitecore.StringExtensions; + +namespace Spe.Client.Applications.UploadFile.Validation +{ + internal class UploadLocationValidator + { + private readonly List _allowedLocations; + private readonly string _webRootPath; + + public UploadLocationValidator(IEnumerable allowedLocations) + { + _webRootPath = HttpContext.Current.Server.MapPath("\\"); + + // Convert relative paths to absolute paths + _allowedLocations = allowedLocations + .Select(path => Path.GetFullPath(Path.IsPathRooted(path) ? path : Path.Combine(_webRootPath, path))) + .ToList(); + } + + public bool Validate(string userDefinedPath) + { + if (string.IsNullOrWhiteSpace(userDefinedPath)) return false; + + string fullPath; + try + { + fullPath = GetFullPath(userDefinedPath); + } + catch (Exception) + { + return false; // Invalid path format + } + + return _allowedLocations.Any(allowedPath => fullPath.StartsWith(allowedPath, StringComparison.OrdinalIgnoreCase)); + } + + public string GetFullPath(string path) + { + return Path.GetFullPath(Path.IsPathRooted(path) ? path : Path.Combine(_webRootPath, path)); + } + } +} diff --git a/src/Spe/Client/Applications/UploadFile/Validation/ValidationResult.cs b/src/Spe/Client/Applications/UploadFile/Validation/ValidationResult.cs new file mode 100644 index 00000000..d4ce0cd5 --- /dev/null +++ b/src/Spe/Client/Applications/UploadFile/Validation/ValidationResult.cs @@ -0,0 +1,8 @@ +namespace Spe.Client.Applications.UploadFile.Validation +{ + internal class ValidationResult + { + public string Message { get; set; } + public bool Valid { get; set; } + } +} diff --git a/src/Spe/Spe.csproj b/src/Spe/Spe.csproj index dbbc688f..1f1bf721 100644 --- a/src/Spe/Spe.csproj +++ b/src/Spe/Spe.csproj @@ -178,6 +178,10 @@ + + + +