Galerieordner in ASP.NET Web Forms: sicher durchsuchen und anlegen

Der Dialog „Insert Gallery“ im Ordner blog des Web-Forms-Beispiels mit vier Bildern und dem Pfad /blog

Der Galerie-Dialog hat eine Schaltfläche New folder und öffnet Ordner per Namen. Beides sendet einen Ordnernamen vom Browser an Ihren Server, und Windows und IIS haben jeweils eigene Regeln dafür, welche Namen funktionieren. Manche Namen lassen sich anstandslos anlegen, können aber nie ein Bild ausliefern. Diese Seite zeigt, wie das Web-Forms-Beispiel einen virtuellen Ordner wie blog/2026 in einen Pfad innerhalb von uploads/ umwandelt und welche Namen es unter Windows ablehnt.

Einen Ordner anlegen

Der Dialog sendet action=create-folder, folder (den übergeordneten Ordner) und name per POST an /rte/gallery. Das übernimmt der POST-Zweig desselben IHttpHandler, der auch die Liste ausliefert, und liest die Werte aus Request.Unvalidated.Form (siehe der Galerie-Handler). Mit dem validierten Request.Form erzeugte ein Name wie <b>x eine HTML-Seite mit 500 aus der Anforderungsüberprüfung. Mit dem Lesen ohne Überprüfung erhält er ein JSON-400, mit dem der Dialog umgehen kann.

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);
}

Bei Erfolg ist die Antwort die Liste des übergeordneten Ordners, sodass der neue Ordner sofort erscheint. Ein vorhandener Name erhält ein 409. NTFS unterscheidet nicht zwischen Groß- und Kleinschreibung, also ist auch Blog neben blog ein 409.

Vom virtuellen Ordner zum Pfad auf der Festplatte

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;
}

Der virtuelle Ordner erreicht das Dateisystem nie so, wie er eingegeben wurde. Jeder durch / getrennte Teil muss zuerst die Regeln für Ordnernamen bestehen. Diese Regeln schließen .., ., leere Teile, \, :, Laufwerksbuchstaben und NUL bereits aus. Das Ergebnis von Path.GetFullPath wird dann mit dem Stammordner verglichen, der aus Server.MapPath stammt, sodass auch eine Lücke in den Regeln den Stammordner nicht verlassen kann. Der Vergleich verwendet OrdinalIgnoreCase, weil Windows-Pfade nicht zwischen Groß- und Kleinschreibung unterscheiden. Im Vertragstest erhielten .., ../.., %2e%2e, C:\Windows, /etc und a/../../x alle ein 400.

Namen, die Windows und IIS besonders behandeln

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;
}

Neben der Grundregel (1 bis 64 Buchstaben, Ziffern, Leerzeichen, -, _ oder ., nicht mit . beginnend) kamen nach Tests drei Regeln für Windows und IIS hinzu:

  • Kein Punkt und kein Leerzeichen am Ende. Laut den Microsoft-Regeln für Dateinamen darf ein Name mit keinem von beiden enden, weil die Windows-APIs sie entfernen. Ein als 2026. angelegter Ordner wäre auf der Festplatte 2026, während der Dialog weiter nach 2026. fragen würde.
  • Keine Gerätenamen wie con, nul oder com1. Das gilt auch für Namen mit Dateiendung wie aux.txt, die Windows ebenfalls als das Gerät behandelt.
  • Keine versteckten Segmente von IIS. Die IIS-Anforderungsfilterung lehnt jede URL mit einem Segment namens bin, App_Data, App_Code, web.config und Ähnlichem ab, in jeder Tiefe. Wir haben ein Bild in uploads/bin/ und in uploads/App_Data/ abgelegt. Beide URLs antworteten mit 404.8, obwohl die Dateien vorhanden waren. Ein Galerieordner namens bin würde daher Bilder auflisten, die nie geladen werden. Das Beispiel lehnt diese Namen mit "bin" is a reserved name on Windows or IIS. ab.

Unicode-Buchstaben sind erlaubt. Wir haben Fotos Ünïcode 2026 angelegt und hineingeladen. Die Bild-URL war /uploads/Fotos%20%C3%9Cn%C3%AFcode%202026/ein-bild-….png, und IIS lieferte sie aus.

Testen

Während das Beispielprojekt zu dieser Anleitung auf Port 5401 läuft, legen Sie im Dialog einen Ordner blog auf oberster Ebene an, dann:

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."}

Das erste ist HTTP 200, mit dem neuen Ordner in der Liste des übergeordneten Ordners. Ein erneutes Senden ergibt ein 409. Das zweite ist HTTP 400. Öffnen Sie im Browser im Dialog blog: Der Pfad zeigt /blog, und Up führt zurück zum Stammordner.

Dieselbe Anleitung für andere Plattformen

JavaScript · PHP · ASP.NET Core · MVC 5

Weiterlesen