Pastas da galeria no ASP.NET Web Forms: navegar e criar com segurança

A caixa de diálogo da galeria tem um botão New folder e abre pastas pelo nome. As duas ações enviam um nome de pasta do navegador para o seu servidor, e o Windows e o IIS têm, cada um, regras sobre quais nomes funcionam. Alguns nomes são criados sem reclamação, mas nunca conseguem servir uma imagem. Esta página mostra como o exemplo Web Forms transforma uma pasta virtual como blog/2026 em um caminho dentro de uploads/, e quais nomes ele recusa no Windows.
Como criar uma pasta
A caixa de diálogo envia action=create-folder, folder (a pasta pai) e name para /rte/gallery. O ramo POST do mesmo IHttpHandler que serve a listagem trata a requisição, lendo os valores de Request.Unvalidated.Form (veja o handler da galeria). Com o Request.Form validado, um nome como <b>x gerava um HTML 500 da validação de requisições. Com a leitura sem validação, ele recebe um JSON 400 que a caixa de diálogo consegue tratar.
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);
}Em caso de sucesso, a resposta é a listagem da pasta pai, então a nova pasta aparece na hora. Um nome que já existe recebe 409. O NTFS não diferencia maiúsculas de minúsculas, então Blog ao lado de blog também gera 409.
Da pasta virtual ao caminho no disco
// 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;
}A pasta virtual nunca chega ao sistema de arquivos do jeito que foi digitada. Cada parte separada por / precisa primeiro passar pelas regras de nome de pasta. Essas regras já excluem .., ., partes vazias, \, :, letras de unidade e NUL. Em seguida, o resultado de Path.GetFullPath é comparado com a raiz, que vem de Server.MapPath, então mesmo uma falha nas regras não consegue sair da raiz. A comparação usa OrdinalIgnoreCase porque os caminhos do Windows não diferenciam maiúsculas de minúsculas. No teste de contrato, .., ../.., %2e%2e, C:\Windows, /etc e a/../../x receberam 400.
Nomes que o Windows e o IIS tratam de forma especial
// 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;
}Além da regra básica (de 1 a 64 letras, dígitos, espaços, -, _ ou ., sem começar com .), três regras do Windows e do IIS foram adicionadas depois dos testes:
- Sem ponto ou espaço no final. As regras de nomenclatura de arquivos da Microsoft dizem que um nome não deve terminar com nenhum dos dois, porque as APIs do Windows os descartam. Uma pasta criada como
2026.ficaria como2026no disco, enquanto a caixa de diálogo continuaria pedindo2026.. - Sem nomes de dispositivo como
con,nuloucom1. Isso inclui nomes com extensão, comoaux.txt, que o Windows também trata como o dispositivo. - Sem segmentos ocultos do IIS. A filtragem de requisições do IIS recusa qualquer URL com um segmento chamado
bin,App_Data,App_Code,web.confige similares, em qualquer nível. Colocamos uma imagem emuploads/bin/e emuploads/App_Data/. As duas URLs responderam 404.8, embora os arquivos estivessem lá. Uma pasta de galeria chamadabinlistaria, portanto, imagens que nunca carregam. O exemplo recusa esses nomes com"bin" is a reserved name on Windows or IIS.
Letras Unicode são permitidas. Criamos Fotos Ünïcode 2026 e enviamos um arquivo para ela. A URL da imagem foi /uploads/Fotos%20%C3%9Cn%C3%AFcode%202026/ein-bild-….png, e o IIS a serviu.
Teste
Com o projeto de exemplo deste guia rodando na porta 5401, crie uma pasta blog no nível superior pela caixa de diálogo e depois:
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."}A primeira é HTTP 200, com a nova pasta na listagem da pasta pai. Enviá-la de novo gera 409. A segunda é HTTP 400. No navegador, abra blog na caixa de diálogo: o caminho mostra /blog, e Up volta para a raiz.
O mesmo guia para outras plataformas
JavaScript · PHP · ASP.NET Core · MVC 5