Dossiers de galerie avec ASP.NET Web Forms : parcourir et créer en toute sécurité

La boîte de dialogue de la galerie a un bouton New folder et ouvre les dossiers par leur nom. Dans les deux cas, un nom de dossier part du navigateur vers votre serveur, et Windows comme IIS ont chacun leurs règles sur les noms qui fonctionnent. Certains noms sont créés sans difficulté mais ne pourront jamais servir une image. Cette page montre comment l’exemple Web Forms transforme un dossier virtuel comme blog/2026 en chemin à l’intérieur de uploads/, et quels noms il refuse sous Windows.
Créer un dossier
La boîte de dialogue envoie action=create-folder, folder (le parent) et name à /rte/gallery. C’est la branche POST du même IHttpHandler qui sert la liste qui s’en charge, en lisant les valeurs dans Request.Unvalidated.Form (voir le gestionnaire de la galerie). Avec le Request.Form validé, un nom comme <b>x provoquait une erreur HTML 500 due à la validation des requêtes. Avec la lecture non validée, il reçoit un 400 JSON que la boîte de dialogue sait traiter.
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);
}En cas de succès, la réponse est la liste du dossier parent : le nouveau dossier apparaît donc aussitôt. Un nom existant reçoit un 409. NTFS ne tient pas compte de la casse : Blog à côté de blog donne donc aussi un 409.
Du dossier virtuel au chemin sur le disque
// 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;
}Le dossier virtuel n’atteint jamais le système de fichiers tel qu’il a été saisi. Chaque partie séparée par / doit d’abord respecter les règles de nommage des dossiers. Ces règles excluent déjà .., ., les parties vides, \, :, les lettres de lecteur et NUL. Le résultat de Path.GetFullPath est ensuite comparé à la racine, issue de Server.MapPath : même une faille dans les règles ne permet donc pas de sortir de la racine. La comparaison utilise OrdinalIgnoreCase, car les chemins Windows ne tiennent pas compte de la casse. Dans le test de contrat, .., ../.., %2e%2e, C:\Windows, /etc et a/../../x ont tous reçu un 400.
Les noms que Windows et IIS traitent à part
// 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;
}En plus de la règle de base (de 1 à 64 lettres, chiffres, espaces, -, _ ou ., sans . au début), trois règles liées à Windows et à IIS ont été ajoutées après les tests :
- Pas de point ni d’espace à la fin. Selon les règles de nommage des fichiers de Microsoft, un nom ne doit se terminer par aucun des deux, car les API Windows les suppriment. Un dossier créé sous le nom
2026.serait2026sur le disque, alors que la boîte de dialogue continuerait à demander2026.. - Pas de noms de périphériques comme
con,nuloucom1. Cela inclut les noms avec une extension, commeaux.txt, que Windows traite aussi comme le périphérique. - Pas de segments masqués d’IIS. Le filtrage des requêtes d’IIS refuse toute URL comportant un segment nommé
bin,App_Data,App_Code,web.configou similaire, à n’importe quelle profondeur. Nous avons placé une image dansuploads/bin/et dansuploads/App_Data/. Les deux URL ont répondu 404.8, alors que les fichiers étaient bien là. Un dossier de galerie nommébinlisterait donc des images qui ne se chargent jamais. L’exemple refuse ces noms avec"bin" is a reserved name on Windows or IIS.
Les lettres Unicode sont autorisées. Nous avons créé Fotos Ünïcode 2026 et envoyé un fichier dedans. L’URL de l’image était /uploads/Fotos%20%C3%9Cn%C3%AFcode%202026/ein-bild-….png, et IIS l’a servie.
Tester
Avec le projet d’exemple de ce guide en cours d’exécution sur le port 5401, créez un dossier blog de premier niveau dans la boîte de dialogue, puis :
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 première réponse est un HTTP 200, avec le nouveau dossier dans la liste du parent. Renvoyer cette requête donne un 409. La seconde est un HTTP 400. Dans le navigateur, ouvrez blog dans la boîte de dialogue : le chemin affiche /blog, et Up revient à la racine.
Le même guide pour d’autres plateformes
JavaScript · PHP · ASP.NET Core · MVC 5