
Improve Data Security by Safely Storing Files in .NET
Author - Abdul Rahman (Bhai)
Security
9 Articles
Table of Contents
What we gonna do?
Accepting file uploads in your application is like opening a door to your server. One wrong move and attackers could overwrite critical system files or access confidential data. In this article, we'll explore how to safely store files in .NET applications, protecting against path traversal attacks and implementing proper file storage security.
Whether you're building a web API, desktop application, or mobile backend, the principles we cover here will help keep your application secure when handling user-uploaded files.
Why we gonna do?
Path traversal attacks are one of the most common and dangerous vulnerabilities in applications that accept file uploads. When users can control the file path where uploads are stored, they can potentially write files anywhere on your server—from overwriting configuration files to replacing system executables.
The Real-World Impact
Imagine this scenario: Your application stores user files in C:\UserFiles\User9\. A malicious user uploads a file named ..\..\config.json. Without proper validation, this resolves to C:\UserFiles\config.json—they've just overwritten your application's configuration file.
Even worse, attackers can target critical directories:
- C:\inetpub\wwwroot - Inject malicious web content
- C:\Windows\System32 - Replace system executables
- Other users' directories - Access or modify other users' data
- Cloud storage buckets - Path traversal works in cloud environments too
The attack is trivial to execute and easy to automate. Attackers can quickly guess user IDs, folder names, and file paths, making this a serious security concern for any application handling file uploads.
Beyond Code: Defense in Depth
While secure coding practices are essential, they're only one layer of defense. Operating system permissions provide a critical safety net. Your application should run with the minimum privileges necessary—a principle known as least privilege.
For example, modern IIS applications run under virtual accounts like IIS AppPool\MyAppPool, with read and execute permissions on C:\inetpub\wwwroot by default. This means even if your code has a vulnerability, the OS prevents writing to critical directories.
Never grant your application write access to directories it doesn't absolutely need. Upload folders should be:
- Separate from your web application directory
- On a different drive when possible (path traversal can't jump between drives)
- Protected with minimal permissions (write-only if files don't need to be read back)
How we gonna do?
Let's explore practical strategies to protect your .NET applications from path traversal attacks and implement secure file storage.
Step 1: Use Random Filenames
The simplest and most secure approach is to completely ignore the user-provided filename and generate a random one. This eliminates path traversal risks entirely.
// Generate a random filename using GUID
var fileName = $"{Guid.NewGuid()}.pdf";
var filePath = Path.Combine(userDirectory, fileName);
// Save the original filename in your database if you need it
await _database.SaveFileMetadataAsync(new FileMetadata
{
StoredFileName = fileName,
OriginalFileName = uploadedFile.FileName,
UserId = currentUser.Id
});
This approach separates file storage from file presentation. Users see their original filename, but internally, files are stored with GUIDs that can't be manipulated for path traversal.
Step 2: Sanitize User-Provided Filenames
If you must preserve the original filename on disk, use .NET's Path.GetFileName() to strip out any path components:
// User uploads: "../../etc/passwd"
var userProvidedName = uploadedFile.FileName;
// Path.GetFileName extracts only the filename, removing path traversal attempts
var safeFileName = Path.GetFileName(userProvidedName);
// Result: "passwd" (path components removed)
// Now use Path.Combine to build the full path
var userDirectory = $"D:\\Uploads\\User{currentUser.Id}";
var filePath = Path.Combine(userDirectory, safeFileName);
// Result: "D:\Uploads\User123\passwd"
Never concatenate file paths using string operations. Always use Path.Combine() which handles path separators correctly across platforms (Windows vs. Linux) and prevents common mistakes.
Step 3: Validate the Final Path
As a final safety check, verify that the resolved file path actually starts with your intended directory. This catches any edge cases that might slip through:
public async Task<IActionResult> UploadFile(IFormFile file)
{
// Define the expected base directory
var baseDirectory = $"D:\\Uploads\\User{User.GetUserId()}";
// Sanitize the filename
var safeFileName = Path.GetFileName(file.FileName);
// Combine to create full path
var filePath = Path.Combine(baseDirectory, safeFileName);
// Get the absolute path and verify it's within our base directory
var fullPath = Path.GetFullPath(filePath);
if (!fullPath.StartsWith(baseDirectory, StringComparison.OrdinalIgnoreCase))
{
return BadRequest("Invalid file path detected");
}
// Safe to save the file
using var stream = new FileStream(fullPath, FileMode.Create);
await file.CopyToAsync(stream);
return Ok(new { FileName = safeFileName });
}
Path.GetFullPath() resolves any relative path components (like ..) to their absolute form. By checking if the result starts with our base directory, we ensure files can only be written where we intend.
Step 4: Configure Operating System Permissions
Even with perfect code, defense in depth requires proper OS-level permissions. Here's how to configure IIS application pools securely:
# Set the application pool to use the default identity (not LocalSystem!)
Set-ItemProperty "IIS:\AppPools\MyAppPool" -Name processModel.identityType -Value ApplicationPoolIdentity
# Grant minimal permissions to the upload directory
$uploadPath = "D:\Uploads"
$appPoolIdentity = "IIS AppPool\MyAppPool"
# Remove inherited permissions
$acl = Get-Acl $uploadPath
$acl.SetAccessRuleProtection($true, $false)
# Grant only Write permission (add Read if you need to serve files back)
$writePermission = New-Object System.Security.AccessControl.FileSystemAccessRule(
$appPoolIdentity,
"Write",
"ContainerInherit,ObjectInherit",
"None",
"Allow"
)
$acl.AddAccessRule($writePermission)
Set-Acl $uploadPath $acl
This configuration ensures that even if an attacker bypasses your code's validation, the operating system prevents writes to unauthorized locations.
Step 5: Additional Security Measures
Beyond path validation, consider these additional protections:
- File Size Limits: Prevent denial-of-service attacks by limiting upload sizes in your web.config or startup configuration
- Content Type Validation: Verify the file's actual content matches its claimed type (don't trust file extensions alone)
- Virus Scanning: Integrate with antivirus solutions to scan uploaded files before storage
- Separate Storage: Store uploads on a different drive or server from your application code
- Audit Logging: Log all file uploads with user IDs, timestamps, and file metadata for security audits
Summary
Safely storing files in .NET applications requires a multi-layered approach. Use random filenames when possible, sanitize user input with Path.GetFileName(), validate paths with Path.GetFullPath(), and always use Path.Combine() instead of string concatenation.
Remember that secure code is only one layer of defense. Configure your application pool with minimal permissions, store uploads outside your web root, and implement additional security measures like file size limits, content type validation, and audit logging. By combining these strategies, you'll protect your application from path traversal attacks and keep your users' data safe.
Defense in depth isn't just a buzzword—it's the difference between a secure application and a compromised server.