CVE-2019-12277: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') (CWE-22)
value: Blogifier 2.3 before 2019-05-11 does not properly restrict APIs, as demonstrated by missing checks for .. in a pathname.
CWE description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
CVSS v3 base score: 9.8
Published: 2019-05-22T15:29Z
Repository: https://github.com/blogifierdotnet/Blogifier
Language: C#
Files changed: src/Core/Api/AssetsController.cs, src/Core/CoreAPI.xml, src/Core/Extensions/StringExtensions.cs, src/Core/Services/StorageService.cs
Commit date: 2019-05-10 23:59:29 -0500
Commit message: Tightening security in APIs

Diff: diff --git a/src/Core/Api/AssetsController.cs b/src/Core/Api/AssetsController.cs
index e120d910..766d1a00 100644
--- a/src/Core/Api/AssetsController.cs
+++ b/src/Core/Api/AssetsController.cs
@@ -5,10 +5,11 @@ using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using System;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using System.Linq;
+using Microsoft.AspNetCore.Authorization;
 
 namespace Core.Api
 {
     [Route("api/[controller]")]
     [ApiController]
@@ -38,24 +39,24 @@ namespace Core.Api
 
             if (string.IsNullOrEmpty(search))
             {
                 if (filter == "filterImages")
                 {
-                    items = await _store.Find(a => a.AssetType == AssetType.Image, pager);
+                    items = await _store.Find(a => a.AssetType == AssetType.Image, pager, "", !User.Identity.IsAuthenticated);
                 }
                 else if (filter == "filterAttachments")
                 {
-                    items = await _store.Find(a => a.AssetType == AssetType.Attachment, pager);
+                    items = await _store.Find(a => a.AssetType == AssetType.Attachment, pager, "", !User.Identity.IsAuthenticated);
                 }
                 else
                 {
-                    items = await _store.Find(null, pager);
+                    items = await _store.Find(null, pager, "", !User.Identity.IsAuthenticated);
                 }
             }
             else
             {
-                items = await _store.Find(a => a.Title.Contains(search), pager);
+                items = await _store.Find(a => a.Title.Contains(search), pager, "", !User.Identity.IsAuthenticated);
             }
 
             if (page < 1 || page > pager.LastPage)
                 return null;
 
@@ -108,15 +109,16 @@ namespace Core.Api
             var item = await _store.Find(a => a.Url == asset, new Pager(1));
             return item.FirstOrDefault();
         }
 
         /// <summary>
-        /// Upload file(s) to user data store
+        /// Upload file(s) to user data store, authentication required
         /// </summary>
         /// <param name="files">Selected files</param>
         /// <returns>Success or internal error</returns>
         [HttpPost("upload")]
+        [Authorize]
         public async Task<IActionResult> Upload(ICollection<IFormFile> files)
         {
             try
             {
                 foreach (var file in files)
@@ -135,10 +137,11 @@ namespace Core.Api
         /// Remove file from user data store, authentication required
         /// </summary>
         /// <param name="url">Relative URL of the file to remove</param>
         /// <returns></returns>
         [HttpDelete("remove")]
+        [Authorize]
         public IActionResult Remove(string url)
         {
             try
             {
                 _store.DeleteFile(url);
diff --git a/src/Core/CoreAPI.xml b/src/Core/CoreAPI.xml
index d6046d50..6ec357a0 100644
--- a/src/Core/CoreAPI.xml
+++ b/src/Core/CoreAPI.xml
@@ -22,11 +22,11 @@
             <param name="post">Post ID</param>
             <returns>Asset Item</returns>
         </member>
         <member name="M:Core.Api.AssetsController.Upload(System.Collections.Generic.ICollection{Microsoft.AspNetCore.Http.IFormFile})">
             <summary>
-            Upload file(s) to user data store
+            Upload file(s) to user data store, authentication required
             </summary>
             <param name="files">Selected files</param>
             <returns>Success or internal error</returns>
         </member>
         <member name="M:Core.Api.AssetsController.Remove(System.String)">
diff --git a/src/Core/Extensions/StringExtensions.cs b/src/Core/Extensions/StringExtensions.cs
index 363ac4c4..23f3dfcb 100644
--- a/src/Core/Extensions/StringExtensions.cs
+++ b/src/Core/Extensions/StringExtensions.cs
@@ -210,8 +210,24 @@ namespace Core
             }
 
             return text;
         }
 
+        public static string SanitizePath(this string str)
+        {
+            if (str.Contains("..") || str.Contains("//"))
+                throw new ApplicationException("Invalid directory path");
+
+            return str;
+        }
+
+        public static string SanitizeFileName(this string str)
+        {
+            if (str.Contains("..") || str.Contains("//") || str.Count(x => x == '.') > 1)
+                throw new ApplicationException("Invalid file name");
+
+            return str;
+        }
+
         #endregion
     }
 }
\ No newline at end of file
diff --git a/src/Core/Services/StorageService.cs b/src/Core/Services/StorageService.cs
index 03a31cf3..3b9d6c62 100644
--- a/src/Core/Services/StorageService.cs
+++ b/src/Core/Services/StorageService.cs
@@ -30,11 +30,11 @@ namespace Core.Services
         IList<string> GetThemes();
         IList<WidgetItem> GetWidgets(string theme);
 
         string GetHtmlTemplate(string template);
 
-        Task<IEnumerable<AssetItem>> Find(Func<AssetItem, bool> predicate, Pager pager, string path = "");
+        Task<IEnumerable<AssetItem>> Find(Func<AssetItem, bool> predicate, Pager pager, string path = "", bool sanitize = false);
 
         Task Reset();
     }
 
     public class StorageService : IStorageService
@@ -252,11 +252,11 @@ namespace Core.Services
                     }
                 }
             }
         }
 
-        public async Task<IEnumerable<AssetItem>> Find(Func<AssetItem, bool> predicate, Pager pager, string path = "")
+        public async Task<IEnumerable<AssetItem>> Find(Func<AssetItem, bool> predicate, Pager pager, string path = "", bool sanitize = false)
         {
             var skip = pager.CurrentPage * pager.ItemsPerPage - pager.ItemsPerPage;
             var files = GetAssets(path);
             var items = MapFilesToAssets(files);
 
@@ -265,10 +265,18 @@ namespace Core.Services
 
             pager.Configure(items.Count);
 
             var page = items.Skip(skip).Take(pager.ItemsPerPage).ToList();
 
+            if (sanitize)
+            {
+                foreach (var p in page)
+                {
+                    p.Path = "";
+                }
+            }
+
             return await Task.FromResult(page);
         }
 
         public void CreateFolder(string path)
         {
@@ -329,10 +337,12 @@ namespace Core.Services
             catch { }
         }
 
         void VerifyPath(string path)
         {
+            path = path.SanitizePath();
+
             if (!string.IsNullOrEmpty(path))
             {
                 var dir = Path.Combine(Location, path);
 
                 if (!Directory.Exists(dir))
@@ -371,11 +381,11 @@ namespace Core.Services
             if (fileName.StartsWith("mceclip0"))
             {
                 Random rnd = new Random();
                 fileName = fileName.Replace("mceclip0", rnd.Next(100000, 999999).ToString());
             }
-            return fileName;
+            return fileName.SanitizeFileName();
         }
 
         string GetUrl(string path, string root)
         {
             var url = path.ReplaceIgnoreCase(Location, "").Replace(_separator, "/");
@@ -442,11 +452,11 @@ namespace Core.Services
                 title = title.Substring(title.LastIndexOf("/"));
             }
 
             title = title.Replace(" ", "-");
 
-            return title.Replace("/", "");
+            return title.Replace("/", "").SanitizeFileName();
         }
 
         List<AssetItem> MapFilesToAssets(IList<string> assets)
         {
             var items = new List<AssetItem>();

Vulnerable code:                     items = await _store.Find(a => a.AssetType == AssetType.Image, pager);
                    items = await _store.Find(a => a.AssetType == AssetType.Attachment, pager);
                    items = await _store.Find(null, pager);
                items = await _store.Find(a => a.Title.Contains(search), pager);
        /// Upload file(s) to user data store
            Upload file(s) to user data store
        Task<IEnumerable<AssetItem>> Find(Func<AssetItem, bool> predicate, Pager pager, string path = "");
        public async Task<IEnumerable<AssetItem>> Find(Func<AssetItem, bool> predicate, Pager pager, string path = "")
            return fileName;
            return title.Replace("/", "");
Fixed code: using Microsoft.AspNetCore.Authorization;
                    items = await _store.Find(a => a.AssetType == AssetType.Image, pager, "", !User.Identity.IsAuthenticated);
                    items = await _store.Find(a => a.AssetType == AssetType.Attachment, pager, "", !User.Identity.IsAuthenticated);
                    items = await _store.Find(null, pager, "", !User.Identity.IsAuthenticated);
                items = await _store.Find(a => a.Title.Contains(search), pager, "", !User.Identity.IsAuthenticated);
        /// Upload file(s) to user data store, authentication required
        [Authorize]
        [Authorize]
            Upload file(s) to user data store, authentication required
        public static string SanitizePath(this string str)
        {
            if (str.Contains("..") || str.Contains("//"))
                throw new ApplicationException("Invalid directory path");

            return str;
        }

        public static string SanitizeFileName(this string str)
        {
            if (str.Contains("..") || str.Contains("//") || str.Count(x => x == '.') > 1)
                throw new ApplicationException("Invalid file name");

            return str;
        }

        Task<IEnumerable<AssetItem>> Find(Func<AssetItem, bool> predicate, Pager pager, string path = "", bool sanitize = false);
        public async Task<IEnumerable<AssetItem>> Find(Func<AssetItem, bool> predicate, Pager pager, string path = "", bool sanitize = false)
            if (sanitize)
            {
                foreach (var p in page)
                {
                    p.Path = "";
                }
            }

            path = path.SanitizePath();

            return fileName.SanitizeFileName();
            return title.Replace("/", "").SanitizeFileName();