{"record":{"id":"a143393ec88d7389","repo":"fullstackhero/dotnet-starter-kit","slug":"file-type-extension-is-not-allowed-allowed-string-join-rules","errorCode":null,"errorMessage":"File type '{extension}' is not allowed. Allowed: {string.Join(\", \", rules.AllowedExtensions)}","messagePattern":"File type '(.+?)' is not allowed\\. Allowed: (.+?)","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/BuildingBlocks/Storage/Local/LocalStorageService.cs","lineNumber":44,"sourceCode":"        ArgumentNullException.ThrowIfNull(environment);\n        _rootPath = string.IsNullOrWhiteSpace(environment.WebRootPath)\n            ? Path.Combine(environment.ContentRootPath, \"wwwroot\")\n            : environment.WebRootPath;\n        _contentTypeProvider = new FileExtensionContentTypeProvider();\n    }\n\n    public async Task<string> UploadAsync<T>(FileUploadRequest request, FileType fileType, CancellationToken cancellationToken = default)\n        where T : class\n    {\n        ArgumentNullException.ThrowIfNull(request);\n\n        var rules = FileTypeMetadata.GetRules(fileType);\n        var extension = Path.GetExtension(request.FileName);\n\n        if (string.IsNullOrWhiteSpace(extension) ||\n            !rules.AllowedExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase))\n        {\n            throw new InvalidOperationException($\"File type '{extension}' is not allowed. Allowed: {string.Join(\", \", rules.AllowedExtensions)}\");\n        }\n\n        if (request.Data.Count > rules.MaxSizeInMB * 1024 * 1024)\n        {\n            throw new InvalidOperationException($\"File exceeds max size of {rules.MaxSizeInMB} MB.\");\n        }\n\n#pragma warning disable CA1308 // folder names are intentionally lower-case for URLs/paths\n        var folder = FolderSanitizer().Replace(typeof(T).Name.ToLowerInvariant(), \"_\");\n#pragma warning restore CA1308\n        var safeFileName = $\"{Guid.NewGuid():N}_{SanitizeFileName(request.FileName)}\";\n        var relativePath = Path.Combine(UploadBasePath, folder, safeFileName);\n        var fullPath = Path.Combine(_rootPath, relativePath);\n\n        Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);\n\n        await File.WriteAllBytesAsync(fullPath, request.Data.ToArray(), cancellationToken);\n","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/BuildingBlocks/Storage/Local/LocalStorageService.cs#L26-L62","documentation":"LocalStorageService.UploadAsync resolves the FileTypeRules for the requested FileType and validates the uploaded file's extension against rules.AllowedExtensions (case-insensitive). If the extension is missing or not whitelisted, it throws InvalidOperationException before any bytes are written. This is a deliberate security guard against uploading disallowed file types.","triggerScenarios":"UploadAsync called with a FileName that has no extension, an empty/whitespace extension, or an extension not in the AllowedExtensions list for that FileType (e.g. uploading a .exe as an Image).","commonSituations":"Users uploading files with no extension (macOS/Linux exports, dotfiles); clients renaming files; mismatch between the file type selected in the UI and the actual file; adding a new extension to the UI but not to FileTypeMetadata rules.","solutions":["Upload a file whose extension is in the AllowedExtensions list for the chosen FileType.","Extend rules.AllowedExtensions in FileTypeMetadata if the type is legitimately allowed.","Ensure the client sends the real file name with its extension, not a blob name or GUID without extension.","Validate the extension in the UI before calling the API."],"exampleFix":"// before\nawait storage.UploadAsync(new UploadRequest { FileName = \"photo\", FileType = FileType.Image, Data = bytes });\n\n// after\nvar ext = Path.GetExtension(\"photo.jpg\"); // send real name with extension\nif (!FileTypeMetadata.GetRules(FileType.Image).AllowedExtensions.Contains(ext))\n    throw new ArgumentException($\"Unsupported image extension: {ext}\");\nawait storage.UploadAsync(new UploadRequest { FileName = \"photo.jpg\", FileType = FileType.Image, Data = bytes });","handlingStrategy":"validation","validationCode":"var ext = Path.GetExtension(fileName);\nvar rules = FileTypeMetadata.GetRules(fileType);\nif (string.IsNullOrWhiteSpace(ext) || !rules.AllowedExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase))\n    throw new ArgumentException($\"Extension '{ext}' not allowed for {fileType}.\");","typeGuard":"bool IsAllowedExtension(string? ext, FileTypeRules rules) =>\n    !string.IsNullOrWhiteSpace(ext) && rules.AllowedExtensions.Contains(ext, StringComparer.OrdinalIgnoreCase);","tryCatchPattern":"try {\n    await storage.UploadAsync(request);\n} catch (InvalidOperationException ex) when (ex.Message.StartsWith(\"File type\")) {\n    return Results.BadRequest(new { error = \"Unsupported file type\", detail = ex.Message });\n}","preventionTips":["Mirror AllowedExtensions in the frontend file-picker accept attribute.","Never strip or rewrite the file name before upload.","Add new supported formats to FileTypeMetadata first, then enable them in the UI."],"tags":["storage","file-validation","security"],"backgroundTag":"invalid-argument-value","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}