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

El cuadro de diálogo Insert Gallery dentro de la carpeta blog del ejemplo de Web Forms, con cuatro imágenes y la ruta /blog

El cuadro de diálogo de la galería tiene un botón New folder y abre las carpetas por su nombre. Ambos envían un nombre de carpeta desde el navegador a tu servidor, y tanto Windows como IIS tienen reglas sobre qué nombres funcionan. Algunos nombres se crean sin protestar, pero nunca pueden servir una imagen. Esta página muestra cómo el ejemplo de Web Forms convierte una carpeta virtual como blog/2026 en una ruta dentro de uploads/, y qué nombres rechaza en Windows.

Crear una carpeta

El cuadro de diálogo envía action=create-folder, folder (la carpeta superior) y name a /rte/gallery. Lo atiende la rama POST del mismo IHttpHandler que sirve el listado, que lee los valores de Request.Unvalidated.Form (consulta el handler de la galería). Con el Request.Form validado, un nombre como <b>x producía un HTML 500 de la validación de solicitudes. Con la lectura sin validar, recibe un 400 en JSON que el cuadro de diálogo puede gestionar.

C# · App_Code/RteFiles.cs
public static void CreateFolder(HttpContext 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 respuesta es el listado de la carpeta superior, así que la carpeta nueva aparece de inmediato. Un nombre existente recibe 409. NTFS no distingue mayúsculas y minúsculas, así que Blog junto a blog también es un 409.

De carpeta virtual a ruta de disco

C# · App_Code/RteFiles.cs
// 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;
}

La carpeta virtual nunca llega al sistema de archivos tal como se escribió. Cada parte separada por / debe pasar primero las reglas de nombres de carpeta. Esas reglas ya descartan .., ., las partes vacías, \, :, las letras de unidad y NUL. Después, el resultado de Path.GetFullPath se compara con la raíz, que viene de Server.MapPath, así que aunque las reglas tuvieran un hueco, no se podría salir de la raíz. La comparación usa OrdinalIgnoreCase porque las rutas de Windows no distinguen mayúsculas y minúsculas. En la prueba de contrato, .., ../.., %2e%2e, C:\Windows, /etc y a/../../x recibieron todos 400.

Nombres que Windows e IIS tratan de forma especial

C# · App_Code/RteFiles.cs
// 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;
}

Además de la regla básica (de 1 a 64 letras, dígitos, espacios, -, _ o ., sin empezar por .), se añadieron tres reglas de Windows e IIS después de las pruebas:

  • Nada de punto o espacio al final. Las reglas de nomenclatura de archivos de Microsoft dicen que un nombre no debe terminar en ninguno de los dos, porque las API de Windows los eliminan. Una carpeta creada como 2026. sería 2026 en el disco, mientras que el cuadro de diálogo seguiría pidiendo 2026..
  • Nada de nombres de dispositivo como con, nul o com1. Esto incluye los nombres con extensión, como aux.txt, que Windows también trata como el dispositivo.
  • Nada de segmentos ocultos de IIS. El filtrado de solicitudes de IIS rechaza cualquier URL con un segmento llamado bin, App_Data, App_Code, web.config y similares, a cualquier profundidad. Pusimos una imagen en uploads/bin/ y en uploads/App_Data/. Ambas URL respondieron 404.8, aunque los archivos estaban ahí. Una carpeta de galería llamada bin mostraría, por tanto, imágenes que nunca se cargan. El ejemplo rechaza estos nombres con "bin" is a reserved name on Windows or IIS.

Se permiten letras Unicode. Creamos Fotos Ünïcode 2026 y subimos un archivo a ella. La URL de la imagen era /uploads/Fotos%20%C3%9Cn%C3%AFcode%202026/ein-bild-….png, e IIS la sirvió.

Pruébalo

Con el proyecto de ejemplo de esta guía en ejecución en el puerto 5401, crea una carpeta blog de primer nivel en el cuadro de diálogo y luego:

Shell
curl -F action=create-folder -F folder=blog -F name=2026 http://localhost:5401/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:5401/rte/gallery
{"error":"\"bin\" is a reserved name on Windows or IIS."}

La primera es HTTP 200, con la carpeta nueva en el listado de la carpeta superior. Volver a enviarla da 409. La segunda es HTTP 400. En el navegador, abre blog en el cuadro de diálogo: la ruta muestra /blog, y Up vuelve a la raíz.

La misma guía para otras plataformas

JavaScript · PHP · ASP.NET Core · MVC 5

Para seguir leyendo