Galerieordner in ASP.NET Core: sicher durchsuchen und anlegen

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:
// 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:
// 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
// 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.")gabC:\app\uploads\photoszurück. Ein alsphotos.angelegter Ordner würde alsphotosaufgeführt, und beide Namen würden auf denselben Ordner zeigen. - Gerätenamen.
con,nul,com1und ähnliche, mit oder ohne Dateiendung, werden abgelehnt.name=conerhält ein 400. - Ein Punkt am Anfang. Das hält
..und versteckte Ordner wie.gitfern. 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:
// 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")isttrue. Vergleichen Sie mit dem Stammordner einschließlich abschließendem Trennzeichen, wie es der Code tut.Path.Combine(@"C:\app\uploads", @"C:\Windows")gibtC:\Windowszurück, undPath.Combine(@"C:\app\uploads", @"\Windows")gibt\Windowszurück. Ein Argument mit Stammverzeichnis verwirft alles davor. Übergeben Sie niemals einen Wert aus dem Request ungeprüft anPath.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.

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:
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