Carpetas de la galería en ASP.NET MVC 5: explorar y crear de forma segura

El cuadro de diálogo de la galería abre carpetas con GET /rte/gallery?folder=blog y las crea con un POST a la misma URL. En ASP.NET MVC 5, eso significa dos acciones con un mismo nombre, y un campo de formulario cuyo nombre también es un valor de ruta reservado de MVC. Esta página muestra cómo lo configura el ejemplo y cómo un nombre de carpeta que llega del navegador se convierte en una ruta dentro de uploads/ sin que se confíe nunca en él como ruta.
Una URL, dos acciones
La ruta rte/gallery apunta al nombre de acción Gallery. El método GET es Gallery(string folder), que se muestra en la guía de la galería. El método POST necesita otro nombre en C#, así que [ActionName("Gallery")] le devuelve el nombre de la ruta, y [HttpPost] permite que MVC elija entre los dos:
[HttpPost]
[ActionName("Gallery")]
public ActionResult GalleryPost(string folder, string name)
{
try
{
// Read "action" from the form directly: as an action parameter it would collide
// with MVC's own {action} route value.
if (Request.Unvalidated.Form["action"] != "create-folder")
throw new RteException(400, "Unknown action.");
RteFiles.CreateFolder(HttpContext, folder ?? "", name);
return JsonStatus(200, RteFiles.Listing(HttpContext, folder ?? ""));
}
catch (RteException ex)
{
return JsonStatus(ex.Status, new { error = ex.Message });
}
}El campo llamado "action"
El cuadro de diálogo envía action=create-folder, folder (la carpeta superior) y name. Recibir string action como parámetro parece lo natural, pero action también es un valor de ruta propio de MVC. Lo probamos con un [HttpPost] Act(string action) desechable. Enviado sin campo action, recibió action=Act, el valor de ruta. Con el campo, recibió create-folder. El valor que obtienes depende tanto de la ruta como del formulario. El ejemplo lee en su lugar Request.Unvalidated.Form["action"], para que solo cuente el campo del formulario, y a un campo ausente se le responde con "Unknown action." (400).
folder y name son parámetros normales. Se enlazan sin validación de solicitudes porque el controlador tiene [ValidateInput(false)]. Sin él, name=<b>x dio una página HTML de error 500 de la validación de solicitudes. Con él, el nombre llega a las reglas propias del ejemplo y vuelve como un 400 en JSON.
Crear la carpeta
public static void CreateFolder(HttpContextBase context, string parentFolder, string name)
{
StorageRoot root = GetRoot(context);
string parentDir = ResolveFolder(root, parentFolder ?? "");
string problem = NameProblem(name);
if (problem != null)
throw new RteException(400, problem);
string target = Path.Combine(parentDir, name);
if (Directory.Exists(target) || File.Exists(target))
throw new RteException(409, "A folder named \"" + name + "\" already exists.");
Directory.CreateDirectory(target);
}Si todo va bien, la acción devuelve el listado de la carpeta superior, así que el cuadro de diálogo muestra la carpeta nueva de inmediato. Una carpeta existente da 409, y en NTFS Blog y blog cuentan como el mismo nombre.
La carpeta nunca se convierte en ruta tal como se escribió
// Turns a virtual folder ("", "products", "products/2026") into a directory inside the
// root. Every segment must be a valid folder name, which already rules out "..", ".",
// empty segments, "\", ":", drive letters, NUL and leading "/". The resolved path is
// then checked against the root anyway, so a mistake in the rules cannot escape it.
static string ResolveFolder(StorageRoot root, string folder)
{
if (folder.Length > 0)
{
foreach (string segment in folder.Split('/'))
{
if (!IsValidName(segment))
throw new RteException(400, "Invalid folder.");
}
}
string full = Path.GetFullPath(Path.Combine(root.Dir, folder.Replace('/', '\\')));
if (!(full.TrimEnd('\\') + "\\").StartsWith(root.Dir, StringComparison.OrdinalIgnoreCase))
throw new RteException(400, "Invalid folder.");
if (!Directory.Exists(full))
throw new RteException(404, "Folder not found.");
return full;
}Cada parte de la carpeta virtual separada por / debe ser un nombre válido, lo que ya excluye .., \, : y las partes vacías. Después, la ruta unida pasa por Path.GetFullPath y debe seguir empezando por la raíz que devolvió Server.MapPath("~/uploads"). Las reglas de nombres son más estrictas que "letras, dígitos, espacio, -, _, ." por dos motivos con los que nos encontramos:
- Se puede crear una carpeta llamada
binoApp_Data, pero el filtrado de solicitudes de IIS trata esos nombres como segmentos ocultos en cualquier parte de una URL. Una imagen colocada enuploads/bin/respondió 404.8. El ejemplo rechaza esos nombres, junto con los nombres de dispositivo de Windows comoconyaux.txt. - Se rechazan los nombres que terminan en
.o en un espacio, porque las reglas de nomenclatura de archivos de Microsoft dicen que Windows elimina esos caracteres, así que la carpeta del disco no coincidiría con el nombre del cuadro de diálogo.
// 1-64 chars of letters, digits, space, '-', '_', '.'; not starting with '.'.
// Windows extras: no trailing '.' or space (Windows silently drops them, so "a." would
// be the same folder as "a"), and no reserved device or IIS-hidden names.
public static bool IsValidName(string name)
{
return NameProblem(name) == null;
}
static string NameProblem(string name)
{
const string rule = "Folder names are 1-64 letters, digits, spaces, '-', '_' or '.', and cannot start with '.'.";
if (string.IsNullOrEmpty(name) || name.Length > 64 || name[0] == '.')
return rule;
foreach (char c in name)
{
if (!char.IsLetterOrDigit(c) && c != ' ' && c != '-' && c != '_' && c != '.')
return rule;
}
char last = name[name.Length - 1];
if (last == '.' || last == ' ')
return "Folder names cannot end with '.' or a space.";
string lower = name.ToLowerInvariant();
string stem = lower.Split('.')[0].Trim();
if (Array.IndexOf(BlockedNames, lower) >= 0 || Array.IndexOf(BlockedNames, stem) >= 0)
return "\"" + name + "\" is a reserved name on Windows or IIS.";
return null;
}Pruébalo
Con el proyecto de ejemplo de esta guía en ejecución en el puerto 5402, crea una carpeta blog en el cuadro de diálogo y luego:
curl -F action=create-folder -F folder=blog -F name=2026 http://localhost:5402/rte/gallery
{"currentFolder":"blog","currentFolderDisplay":"/blog","parentFolder":"","folders":[{"folder":"blog/2026","name":"2026"}],"images":[...]}
curl -F action=create-folder -F folder= -F name=bin http://localhost:5402/rte/gallery
{"error":"\"bin\" is a reserved name on Windows or IIS."}HTTP 200 con la carpeta nueva en el listado y luego HTTP 400. Un PUT a /rte/gallery recibe el 404 normal de MVC, porque ninguna de las dos acciones acepta ese verbo. En el cuadro de diálogo, New folder pide un nombre y muestra la carpeta de inmediato.
La misma guía para otras plataformas
JavaScript · PHP · ASP.NET Core · Web Forms