Galerieordner in ASP.NET Core: sicher durchsuchen und anlegen

Der Dialog „Insert Gallery“ im Ordner blog des ASP.NET-Core-Beispiels mit vier Fotos und dem Pfad /blog, das erste davon ausgewählt

Die Schaltfläche „New folder“ der Galerie und ihre Ordnerkacheln senden einen Ordnernamen vom Browser an Ihren Server. In ASP.NET Core muss dieser Name zu einem Pfad auf der Festplatte werden, ohne uploads/ jemals zu verlassen, und Windows hat eigene Überraschungen parat, was ein Ordnername ist. Diese Seite zeigt die Route zum Anlegen von Ordnern, die Namensregeln und die Pfadprüfung, mit den Antworten, die wir bei ungültigen Eingaben erhalten haben.

Die Route

„New folder“ sendet action=create-folder, den geöffneten Ordner und den neuen Namen als multipart/form-data per POST an die Galerie-URL:

C# · Controllers/RteController.cs
// POST /rte/gallery   action=create-folder, folder, name
[HttpPost("gallery")]
public IActionResult CreateFolder([FromForm(Name = "action")] string? op, [FromForm] string? folder, [FromForm] string? name) =>
    Send(RteFiles.CreateFolder(HttpContext, op, folder, name));

Der C#-Parameter heißt op und wird mit [FromForm(Name = "action")] gebunden, wodurch er vom eigenen Routenwert action von MVC getrennt bleibt. Die Methode antwortet mit der Liste des übergeordneten Ordners, sodass der Dialog den neuen Ordner sofort anzeigt:

C# · RteFiles.cs
// POST /rte/gallery   action=create-folder, folder=<parent>, name=<new folder>
public static RteResult CreateFolder(HttpContext http, string? action, string? folder, string? name) => Run(() =>
{
    RequireSameOrigin(http.Request);
    if (action != "create-folder") throw new RteError(400, "Unknown action.");
    var root = RootFor(http);
    folder ??= "";
    var parent = Resolve(root, folder);
    if (!IsValidName(name))
        throw new RteError(400, "Use 1-64 letters, digits, spaces, '-', '_' or '.', not starting with '.'.");
    var path = Path.Combine(parent, name!);
    if (Directory.Exists(path) || File.Exists(path))
        throw new RteError(409, "A folder with that name already exists.");
    Directory.CreateDirectory(path);
    return Listing(root, folder, parent); // the parent's listing, so the new folder shows
});

Unter Windows unterscheidet Directory.Exists nicht zwischen Groß- und Kleinschreibung, sodass Blog neben einem vorhandenen blog dasselbe 409 erhält wie ein exaktes Duplikat.

Ordnernamen

C# · RteFiles.cs
// Folder names: 1-64 ASCII letters, digits, space, - _ . ; not starting with "." (no "..", no hidden
// folders). Also refused because Windows mangles them: a trailing "." or space, a leading space,
// and device names such as "con" or "nul.txt".
public static bool IsValidName(string? name) =>
    name is { Length: >= 1 and <= 64 }
    && name.All(c => char.IsAsciiLetterOrDigit(c) || c is ' ' or '-' or '_' or '.')
    && !name.StartsWith('.') && !name.StartsWith(' ')
    && !name.EndsWith('.') && !name.EndsWith(' ')
    && !WindowsDeviceNames.Contains(name.Split('.')[0]);

Indem nur ASCII-Buchstaben, Ziffern, Leerzeichen, -, _ und . erlaubt sind, werden /, \, :, % und NUL in einem Schritt ausgeschlossen. Die übrigen Regeln ergeben sich daraus, wie Windows Namen behandelt:

  • Ein Punkt oder Leerzeichen am Ende. Path.GetFullPath(@"C:\app\uploads\photos.") gab C:\app\uploads\photos zurück. Ein als photos. angelegter Ordner würde als photos aufgeführt, und beide Namen würden auf denselben Ordner zeigen.
  • Gerätenamen. con, nul, com1 und ähnliche, mit oder ohne Dateiendung, werden abgelehnt. name=con erhält ein 400.
  • Ein Punkt am Anfang. Das hält .. und versteckte Ordner wie .git fern. Die Liste überspringt auch vorhandene Ordner, die gegen die Regeln verstoßen.

Vom virtuellen Ordner zum Pfad auf der Festplatte

Der Browser sendet immer nur virtuelle Pfade: "", blog, blog/2026. Jede Route löst sie auf dieselbe Weise auf:

C# · RteFiles.cs
// A virtual folder ("", "products", "products/2026") becomes a disk path inside the root.
// Every segment must be a valid folder name, so "..", "C:", "\", "%2e%2e" and NUL never reach the disk.
static string Resolve(RteRoot root, string folder)
{
    var rootPath = Path.GetFullPath(root.Disk);
    Directory.CreateDirectory(rootPath);
    if (folder.Length == 0) return rootPath;

    var segments = folder.Split('/');
    if (!segments.All(IsValidName)) throw new RteError(400, "Invalid folder.");

    // Second line of defence: the resolved path must still be inside the root. Compare against the
    // root WITH a trailing separator, or "C:\app\uploads-old" would pass as inside "C:\app\uploads".
    var path = Path.GetFullPath(Path.Combine([rootPath, .. segments]));
    if (!path.StartsWith(Path.TrimEndingDirectorySeparator(rootPath) + Path.DirectorySeparatorChar, StringComparison.Ordinal))
        throw new RteError(400, "Invalid folder.");
    if (!Directory.Exists(path)) throw new RteError(404, "Folder not found.");
    return path;
}

Jedes Segment muss die Namensregeln bestehen, sodass .., C:, %2e%2e und ein führendes / abgelehnt werden, bevor überhaupt ein Pfad gebildet wird. Die Eingrenzungsprüfung nach Path.GetFullPath ist eine zweite Verteidigungslinie, und in .NET sind beide ihrer Details wichtig:

  • @"C:\app\uploads-old\x.png".StartsWith(@"C:\app\uploads") ist true. Vergleichen Sie mit dem Stammordner einschließlich abschließendem Trennzeichen, wie es der Code tut.
  • Path.Combine(@"C:\app\uploads", @"C:\Windows") gibt C:\Windows zurück, und Path.Combine(@"C:\app\uploads", @"\Windows") gibt \Windows zurück. Ein Argument mit Stammverzeichnis verwirft alles davor. Übergeben Sie niemals einen Wert aus dem Request ungeprüft an Path.Combine.

GetFullPath löst .. auf, aber keine Junctions oder symbolischen Links. Eine Junction, die jemand mit Serverzugriff in uploads/ anlegt, lässt sich weiterhin durchsuchen.

Der Ordner uploads des ASP.NET-Core-Beispiels auf der Festplatte: blog mit vier Fotos, leere Ordner products und team und vier Fotos im Stammordner, alle mit generierten Namen
Was die Ordner der Galerie auf der Festplatte sind: einfache Ordner unter uploads/.

Testen

Starten Sie das Beispielprojekt zu dieser Anleitung mit dotnet run. Öffnen Sie in der Galerie blog und klicken Sie auf „New folder“. Oder verwenden Sie curl. Das erste Anlegen antwortet mit der Liste von blog, das zweite mit einem Konflikt:

Shell
curl -F "action=create-folder" -F "folder=blog" -F "name=2026" http://localhost:5302/rte/gallery
{"currentFolder":"blog","currentFolderDisplay":"/blog","parentFolder":"","folders":[{"folder":"blog/2026","name":"2026"}],"images":[...]}

curl -F "action=create-folder" -F "folder=blog" -F "name=2026" http://localhost:5302/rte/gallery
{"error":"A folder with that name already exists."}

curl -F "action=create-folder" -F "folder=" -F "name=.." http://localhost:5302/rte/gallery
{"error":"Use 1-64 letters, digits, spaces, '-', '_' or '.', not starting with '.'."}

curl "http://localhost:5302/rte/gallery?folder=..%2F.."
{"error":"Invalid folder."}

curl "http://localhost:5302/rte/gallery?folder=blog%2Fnope"
{"error":"Folder not found."}

Die Statuscodes sind 200, 409, 400, 400 und 404. Die Bilder in der ersten Antwort sind hier weggelassen; der Dialog verwendet sie, um den Ordner darzustellen.

Dieselbe Anleitung für andere Plattformen

JavaScript · PHP · MVC 5 · Web Forms

Weiterlesen