When building modern ASP.NET Core applications, it’s common to serve files such as documents, PDFs, images, or reports to authenticated users. However, if files are stored in a public wwwroot
folder or exposed directly through static file middleware, they can be accessed by anyone who knows the URL, even without proper authorization.
This can lead to data leaks, information disclosure, and compliance issues. To prevent this, we must implement a secure file-serving mechanism with authorization checks.
Why Not Serve Sensitive Files From?
The wwwroot
folder is meant for public static assets (like CSS, JS, and images). Anything inside it is directly accessible via URL.
For sensitive files (e.g., invoices, medical records, private reports):
Step 1: Project Setup
Create a new project (if not already):
dotnet new webapp -n SecureFileDemo
Enable authentication (if required) or use custom policies/roles.
Step 2: Store Files Outside wwwroot
Suppose we store private files inside SecureFiles
at the root of the project:
/SecureFiles
/Reports
report1.pdf
report2.pdf
These files are not directly accessible via the browser.
Step 3: Create a Secure File Controller
We’ll build an endpoint that:
Validates the user identity.
Checks authorization (roles, claims, policies).
Serves the file securely.
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.IO;
using System.Threading.Tasks;
namespace SecureFileDemo.Controllers
{
[Authorize] // ensures only logged-in users
public class FilesController : Controller
{
private readonly string _fileStoragePath = Path.Combine(Directory.GetCurrentDirectory(), "SecureFiles");
[Authorize(Roles = "Admin,Manager")] // Example: restrict further
[HttpGet("files/{fileName}")]
public async Task<IActionResult> GetFile(string fileName)
{
// Prevent path traversal attack
var safeFileName = Path.GetFileName(fileName);
var filePath = Path.Combine(_fileStoragePath, safeFileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound("File not found.");
}
var memory = new MemoryStream();
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
var contentType = "application/octet-stream"; // default
return File(memory, contentType, safeFileName);
}
}
}
Step 4: Fine-Grained Authorization (Per-User Files)
If each user should only access their own files (e.g., invoices), we can enforce checks:
[HttpGet("userfiles/{fileName}")]
public async Task<IActionResult> GetUserFile(string fileName)
{
var safeFileName = Path.GetFileName(fileName);
var userId = User.Identity.Name; // Or a claim like User.FindFirst("UserId")
var userFolder = Path.Combine(_fileStoragePath, userId);
var filePath = Path.Combine(userFolder, safeFileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound("File not found.");
}
// Only serve if it belongs to this user
if (!filePath.StartsWith(userFolder))
{
return Forbid();
}
var memory = new MemoryStream();
using (var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
await stream.CopyToAsync(memory);
}
memory.Position = 0;
return File(memory, "application/octet-stream", safeFileName);
}
Step 5: Using IAuthorizationService
for Policies
You can implement custom policies for advanced checks (e.g., document ownership):
private readonly IAuthorizationService _authorizationService;
public FilesController(IAuthorizationService authorizationService)
{
_authorizationService = authorizationService;
}
[HttpGet("secure-docs/{fileName}")]
public async Task<IActionResult> GetSecureDoc(string fileName)
{
var requirement = new DocumentAccessRequirement(fileName);
var result = await _authorizationService.AuthorizeAsync(User, null, requirement);
if (!result.Succeeded)
return Forbid();
// Serve the file (same as above)
}
Best Practices
Always store sensitive files outside wwwroot
.
Used Path.GetFileName()
to prevent path traversal attacks.
Implement role/claim/policy-based authorization.
Set appropriate Content-Type headers.
Consider streaming large files instead of loading them into memory.
Enable logging for file access attempts.
Conclusion
By serving files through a controller with proper authentication and authorization, you can ensure that only authorized users can access sensitive content. This approach is much safer than relying on static file hosting for private documents.
With these patterns, you can securely deliver files in ASP.NET Core while maintaining compliance and protecting user data.