I recently discovered a vulnerability in some old .NET 4.8 web forms code that was used to provide basic file management within CKEditor (https://github.com/simogeo/Filemanager). Since I don’t have the ability to rebuild the entire system at this time, I decided to implement a newer file manager instead (https://github.com/psolom/RichFilemanager/). However, the ashx connector provided therein (https://github.com/psolom/RichFilemanager/blob/master/connectors/ashx/filemanager.ashx) didn’t work for me out of the box, so I added the following:
- I changed the file handler to point to a sub-folder so the entire root directory is not displayed
- I incorporated SixLabors.ImageSharp to resize large images
- I added a basic authentication check
- I added support for uploading PDFs
<%@ WebHandler Language="C#" Class="filemanager" %>
// ** Filemanager ASP.NET connector
//
// ** .NET Framework >= 2.0
//
// ** license MIT License
// ** author Ondřej "Yumi Yoshimido" Brožek | <cholera@hzspraha.cz>
// ** Copyright Author
using System;
using System.Web;
using System.IO;
using System.Text;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
public class filemanager : IHttpHandler
{
//===================================================================
//==================== EDIT CONFIGURE HERE ==========================
//===================================================================
public string IconDirectory = "./images/fileicons/"; // Icon directory for filemanager. [string]
public string[] imgExtensions = new string[] { ".jpg", ".png", ".jpeg", ".gif", ".bmp" }; // Only allow this image extensions. [string]
public long size = 0L;
public int files = 0;
public int folders = 0;
public int maxHeight = 1280;
public int maxWidth = 1920;
private string overridePath = "/FileManager/userfiles/";
// added default constructor to specify a sub-folder, depending on whether the code is running locally or not
public filemanager()
{
string serverName = HttpContext.Current.Request.ServerVariables["SERVER_NAME"].ToLower();
overridePath = serverName.Contains("wsprod") ? "/cwis199/email-list/FileManager/UserFiles/" : "/FileManager/userfiles/";
}
//===================================================================
//========================== END EDIT ===============================
//===================================================================
private static void NoCache()
{
HttpContext.Current.Response.AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1.
HttpContext.Current.Response.AppendHeader("Pragma", "no-cache"); // HTTP 1.0.
HttpContext.Current.Response.AppendHeader("Expires", "0"); // Proxies.
HttpContext.Current.Response.Cache.SetExpires(DateTime.UtcNow.AddDays(-1));
HttpContext.Current.Response.Cache.SetValidUntilExpires(false);
HttpContext.Current.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.NoCache);
HttpContext.Current.Response.Cache.SetNoStore();
}
private bool IsImage(FileInfo fileInfo)
{
foreach (string ext in imgExtensions)
{
if (Path.GetExtension(fileInfo.FullName).ToLower() == ext.ToLower())
{
return true;
}
}
return false;
}
private string getInfo(string path)
{
DirectoryInfo RootDirInfo = new DirectoryInfo(HttpContext.Current.Server.MapPath(path));
StringBuilder sb = new StringBuilder();
sb.AppendLine("{ \"data\": [");
int i = 0;
foreach (DirectoryInfo DirInfo in RootDirInfo.GetDirectories())
{
if (i > 0)
{
sb.Append(",");
}
sb.Append(getDirectoryInfo(DirInfo, Path.Combine(path, DirInfo.Name).Replace('\\', '/')));
i++;
}
foreach (FileInfo fileInfo in RootDirInfo.GetFiles())
{
if (i > 0)
{
sb.Append(",");
}
sb.Append(getFileInfo(fileInfo, Path.Combine(path, fileInfo.Name).Replace('\\', '/')));
i++;
}
sb.AppendLine("] }");
return sb.ToString();
}
private string HandleUpload(string path, HttpFileCollection uploadfiles)
{
StringBuilder sb = new StringBuilder();
sb.Append("{ \"data\": [");
foreach (string file in uploadfiles)
{
HttpPostedFile hpf = uploadfiles[file] as HttpPostedFile;
string FileName = string.Empty;
if (HttpContext.Current.Request.Browser.Browser.ToUpper() == "IE")
{
string[] files = hpf.FileName.Split(new char[] { '\\' });
FileName = files[files.Length - 1];
}
else FileName = hpf.FileName;
if (hpf.ContentLength == 0) continue;
hpf.SaveAs(HttpContext.Current.Server.MapPath(Path.Combine(path, Path.GetFileName(FileName))));
hpf.InputStream.Dispose();
FileInfo fileInfo = new FileInfo(HttpContext.Current.Server.MapPath(Path.Combine(path, Path.GetFileName(FileName))));
sb.Append(getFileInfo(fileInfo, Path.Combine(path, FileName)));
}
sb.Append("] }");
return sb.ToString();
}
private string getDirectoryInfo(DirectoryInfo dirInfo, string path)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("{");
sb.AppendLine("\"id\": \"" + path + "/\",");
sb.AppendLine("\"type\": \"folder\",");
sb.AppendLine("\"attributes\": {");
sb.AppendLine("\"name\": \"" + dirInfo.Name + "\",");
sb.AppendLine("\"path\": \"" + path + "/\",");
sb.AppendLine("\"readable\": 1,");
sb.AppendLine("\"writable\": 1,");
sb.AppendLine("\"created\": \"" + dirInfo.CreationTime.ToString() + "\", ");
sb.AppendLine("\"modified\": \"" + dirInfo.LastWriteTime.ToString() + "\", ");
sb.AppendLine("\"timestamp\": " + ((Int32)(dirInfo.CreationTime.Subtract(new DateTime(1970, 1, 1))).TotalSeconds).ToString());
sb.Append("} }");
return sb.ToString();
}
private string getFileInfo(FileInfo fileInfo, string path)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("{");
sb.AppendLine("\"id\": \"" + path + "\",");
sb.AppendLine("\"type\": \"file\",");
sb.AppendLine("\"attributes\": {");
sb.AppendLine("\"name\": \"" + fileInfo.Name + "\",");
sb.AppendLine("\"extension\": \"" + fileInfo.Extension.Replace(".", "") + "\",");
sb.AppendLine("\"path\": \"" + path + "\",");
sb.AppendLine("\"readable\": 1,");
sb.AppendLine("\"writable\": 1,");
sb.AppendLine("\"created\": \"" + fileInfo.CreationTime.ToString() + "\", ");
sb.AppendLine("\"modified\": \"" + fileInfo.LastWriteTime.ToString() + "\", ");
sb.AppendLine("\"timestamp\": " + ((Int32)(fileInfo.CreationTime.Subtract(new DateTime(1970, 1, 1))).TotalSeconds).ToString() + ", ");
if (IsImage(fileInfo))
{
try
{
// added more efficient image handling (e.g., respect max height/width for images)
using (var image = Image.Load(fileInfo.FullName))
{
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(maxWidth, maxHeight),
Mode = ResizeMode.Max
}));
sb.AppendLine("\"height\": " + image.Height.ToString() + ",");
sb.AppendLine("\"width\": " + image.Width.ToString() + ",");
}
}
catch (Exception ex)
{
Elmah.ErrorSignal.FromCurrentContext().Raise(ex);
}
}
else
{
sb.AppendLine("\"height\": 0,");
sb.AppendLine("\"width\": 0,");
}
sb.AppendLine("\"size\": \"" + fileInfo.Length.ToString() + "\"");
sb.AppendLine("} }");
return sb.ToString();
}
private string Rename(string path, string newName)
{
FileAttributes attr = File.GetAttributes(HttpContext.Current.Server.MapPath(path));
StringBuilder sb = new StringBuilder();
sb.AppendLine("{ \"data\":");
if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
{
DirectoryInfo dirInfo = new DirectoryInfo(HttpContext.Current.Server.MapPath(path));
Directory.Move(HttpContext.Current.Server.MapPath(path), Path.Combine(dirInfo.Parent.FullName, newName));
DirectoryInfo fileInfo2 = new DirectoryInfo(Path.Combine(dirInfo.Parent.FullName, newName));
path = path.Remove(path.Length - 1); // remove last slash
sb.Append(getDirectoryInfo(fileInfo2, path.Remove(path.LastIndexOf('/'))));
}
else
{
FileInfo fileInfo = new FileInfo(HttpContext.Current.Server.MapPath(path));
File.Move(HttpContext.Current.Server.MapPath(path), Path.Combine(fileInfo.Directory.FullName, newName));
FileInfo fileInfo2 = new FileInfo(Path.Combine(fileInfo.Directory.FullName, newName));
path = path.Remove(path.Length - 1); // remove last slash
sb.Append(getFileInfo(fileInfo2, path.Remove(path.LastIndexOf('/'))));
}
sb.Append("}");
return sb.ToString();
}
private string Move(string path, string newPath)
{
FileAttributes attr = File.GetAttributes(HttpContext.Current.Server.MapPath(path));
StringBuilder sb = new StringBuilder();
sb.AppendLine("{ \"data\":");
if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
{
DirectoryInfo dirInfo = new DirectoryInfo(HttpContext.Current.Server.MapPath(path));
Directory.Move(HttpContext.Current.Server.MapPath(path), Path.Combine(dirInfo.Parent.FullName, newPath));
DirectoryInfo fileInfo2 = new DirectoryInfo(Path.Combine(dirInfo.Parent.FullName, newPath));
sb.Append(getDirectoryInfo(fileInfo2, path.Remove(path.LastIndexOf('/'))));
}
else
{
string filename = path.Substring(path.LastIndexOf('/'));
FileInfo fileInfo = new FileInfo(HttpContext.Current.Server.MapPath(path));
File.Move(HttpContext.Current.Server.MapPath(path), HttpContext.Current.Server.MapPath(newPath) + "/" + filename);
FileInfo fileInfo2 = new FileInfo(HttpContext.Current.Server.MapPath(newPath) + "/" + filename);
sb.Append(getFileInfo(fileInfo2, newPath));
}
sb.Append("}");
return sb.ToString();
}
private string Delete(string path)
{
FileAttributes attr = File.GetAttributes(HttpContext.Current.Server.MapPath(path));
StringBuilder sb = new StringBuilder();
sb.Append("{ \"data\":");
if ((attr & FileAttributes.Directory) == FileAttributes.Directory)
{
path = path.Remove(path.LastIndexOf('/'), 1);
DirectoryInfo dirInfo = new DirectoryInfo(HttpContext.Current.Server.MapPath(path));
sb.Append(getDirectoryInfo(dirInfo, path));
Directory.Delete(HttpContext.Current.Server.MapPath(path), true);
}
else
{
FileInfo fileInfo = new FileInfo(HttpContext.Current.Server.MapPath(path));
sb.Append(getFileInfo(fileInfo, path));
File.Delete(HttpContext.Current.Server.MapPath(path));
}
sb.Append("}");
return sb.ToString();
}
private string AddFolder(string path, string NewFolder)
{
StringBuilder sb = new StringBuilder();
Directory.CreateDirectory(Path.Combine(HttpContext.Current.Server.MapPath(path), NewFolder));
DirectoryInfo dirInfo = new DirectoryInfo(Path.Combine(HttpContext.Current.Server.MapPath(path), NewFolder));
sb.AppendLine("{ \"data\":");
sb.Append(getDirectoryInfo(dirInfo, Path.Combine(path, NewFolder)));
sb.Append("}");
return sb.ToString();
}
private string Replace(string path, HttpFileCollection uploadfiles)
{
StringBuilder sb = new StringBuilder();
sb.Append(Delete(path));
HttpPostedFile hpf = uploadfiles[0] as HttpPostedFile;
string FileName = path.Substring(path.LastIndexOf("/"));
path = path.Substring(0, path.LastIndexOf("/"));
if (HttpContext.Current.Request.Browser.Browser.ToUpper() == "IE")
{
string[] files = FileName.Split(new char[] { '\\' });
FileName = files[files.Length - 1];
}
hpf.SaveAs(HttpContext.Current.Server.MapPath(Path.Combine(path, FileName)));
hpf.InputStream.Dispose();
return sb.ToString();
}
private string Summarize()
{
DirectoryInfo RootDirInfo = new DirectoryInfo(HttpContext.Current.Server.MapPath("/"));
size = 0L;
files = 0;
folders = 0;
GetSummaryInfo(HttpContext.Current.Server.MapPath("/"));
StringBuilder sb = new StringBuilder();
sb.Append("{ \"data\": {");
sb.Append("\"id\": \"/\",");
sb.Append("\"type\": \"summary\",");
sb.Append("\"attributes\": {");
sb.Append("\"size\": " + size + ",");
sb.Append("\"files\": " + files + ",");
sb.Append("\"folders\": " + folders + ",");
sb.Append("\"sizelimit\": 16000000"); // TODO read from config
sb.Append("} } }");
return sb.ToString();
}
private string GetSummaryInfo(string path)
{
string[] newpath = Directory.GetDirectories(path);
foreach (string fp in newpath)
{
folders++;
GetSummaryInfo(fp);
}
newpath = Directory.GetFiles(path);
foreach (string fp in newpath)
{
files++;
FileInfo finfo = new FileInfo(fp);
size += finfo.Length;
}
return size.ToString() + "," + files.ToString() + "," + folders.ToString();
}
private string Initiate()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("{");
sb.Append("\"data\": {");
sb.AppendLine("\"id\": \"/\",");
sb.AppendLine("\"type\": \"initiate\",");
sb.AppendLine("\"attributes\": {");
sb.Append("\"config\": {");
sb.AppendLine("\"security\": {");
sb.AppendLine("\"readOnly\": false,");
sb.AppendLine("\"extensions\": {");
sb.AppendLine("\"policy\": \"ALLOW_LIST\",");
sb.AppendLine("\"restrictions\": [\"jpg\",\"jpe\",\"jpeg\",\"gif\",\"png\",\"pdf\"]"); // added support for PDFs
sb.AppendLine("}");
sb.AppendLine("}");
sb.AppendLine("}");
sb.AppendLine("}");
sb.AppendLine("}");
sb.AppendLine("}");
return sb.ToString();
}
public byte[] ReadAllBytes(string fileName)
{
byte[] buffer = null;
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
{
buffer = new byte[fs.Length];
fs.Read(buffer, 0, (int)fs.Length);
}
return buffer;
}
public void ProcessRequest(HttpContext context)
{
FileInfo fi = null;
context.Response.ClearHeaders();
context.Response.ClearContent();
context.Response.Clear();
// added support for a basic auth check
if (!context.Request.IsAuthenticated)
context.Response.Redirect("~/Login.aspx");
string path = context.Request["path"];
if (path != null)
{
path = Path.Combine(overridePath, context.Request["path"].TrimStart('/'));
path = path.Replace(overridePath + overridePath.TrimStart('/'), overridePath); // in case duplicate strings get in path
}
switch (context.Request["mode"])
{
case "initiate":
context.Response.ContentType = "plain/text";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(Initiate());
break;
case "readfolder":
context.Response.ContentType = "plain/text";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(getInfo(path));
break;
case "rename":
context.Response.ContentType = "plain/text";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(Rename(context.Request["old"], context.Request["new"]));
break;
case "delete":
context.Response.ContentType = "plain/text";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(Delete(path));
break;
case "addfolder":
context.Response.ContentType = "plain/text";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(AddFolder(path, context.Request["name"]));
break;
case "upload":
context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
context.Response.ContentType = "text/html";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(HandleUpload(path, context.Request.Files));
break;
case "move":
context.Response.ContentType = "plain/text";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(Move(context.Request["old"], context.Request["new"]));
break;
case "getimage":
NoCache();
//context.Response.Write(Preview(context.Request["thumbnail"] == "true"));
fi = new FileInfo(context.Server.MapPath(path));
context.Response.AddHeader("Content-Disposition", "attachment; filename=" + context.Server.UrlPathEncode(fi.Name));
context.Response.AddHeader("Content-Length", fi.Length.ToString());
context.Response.ContentType = System.Web.MimeMapping.GetMimeMapping(fi.Name);
context.Response.TransmitFile(fi.FullName);
break;
case "replace":
context.Response.ContentType = "plain/text";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(Replace(path, context.Request.Files));
break;
case "summarize":
context.Response.ContentType = "plain/text";
context.Response.ContentEncoding = Encoding.UTF8;
context.Response.Write(Summarize());
break;
case "download": //call in window.open
fi = new FileInfo(context.Server.MapPath(path));
System.Web.HttpResponse response = System.Web.HttpContext.Current.Response;
response.Clear();
response.ClearContent();
response.ClearHeaders();
response.AppendHeader("Content-Type", System.Web.MimeMapping.GetMimeMapping(fi.Name) + "; charset=UTF-8");
response.Charset = "UTF-8";
response.ContentEncoding = System.Text.Encoding.UTF8;
response.AppendHeader("Content-Disposition", "attachment; filename=" + fi.Name + "; size=" + fi.Length.ToString());
context.Response.AddHeader("Content-Length", fi.Length.ToString());
response.CacheControl = "Public";
response.BinaryWrite(ReadAllBytes(fi.FullName));
response.Flush();
response.End();
break;
default:
break;
}
}
public bool IsReusable
{
get
{
return false;
}
}
}