7 min read

Preventing Path Traversal Attacks in File Uploads: A Technical Deep Dive

When your application reads from or writes to a file system location based on user input and that input isn't carefully checked, an attacker can manipulate the path to access directories or files they shouldn't be able to access. It's fundamentally a file system injection problem.
Preventing Path Traversal Attacks in File Uploads: A Technical Deep Dive

File uploads are a common feature in many web applications, but they also introduce a significant security risk: path traversal attacks. These attacks can allow malicious actors to access or modify files outside of the intended directory, potentially leading to privilege escalation, data exposure, or even remote code execution. In this post, we'll break down what path traversal is, how it works, and, most importantly, how to defend your applications against it, all while staying true to the practical, no-nonsense style of our guide.

If you prefer video guidance, you can refer to this YouTube tutorial.

Understanding Path Traversal Attacks

At its core, a path traversal attack, also known as directory traversal, is a vulnerability that arises when an application doesn't properly validate user-supplied path inputs. When your application reads from or writes to a file system location based on user input and that input isn't carefully checked, an attacker can manipulate the path to access directories or files they shouldn't be able to access. It's fundamentally a file system injection problem.

The attacker's goal is to use special character sequences, most commonly ../ (dot dot slash), to "escape" the intended directory and navigate to sensitive parts of your server's file system. Imagine an attacker uploading a file, but instead of just providing a filename, they craft it like ../../../../etc/passwd on a Linux system or ..\..\..\..\Windows\System32\config\SAM on Windows. If your application blindly uses this in the file path, it could end up reading or writing to critical system files.

The impact of such an attack can be severe:

  • Overwriting critical application or OS files: This can lead to denial-of-service or even compromise the entire system.
  • Exposing sensitive system information: Attackers might be able to read configuration files, user credentials, or other confidential data.
  • Enabling remote code execution: By planting malicious files in specific locations, attackers can sometimes execute arbitrary code on your server.

The Vulnerable Code: A Practical Example

Let's dive into a real-world scenario to see how this vulnerability manifests. In our FilesController, we're setting up an upload route.

// Setting the upload route to be inside the content root path
var uploadRoute = Path.Combine(_webHostEnvironment.ContentRootPath, "AppData", "Uploads");

// If the destination path doesn't exist, create it
if (!Directory.Exists(uploadRoute))
{
    Directory.CreateDirectory(uploadRoute);
}

Here, uploadRoute is configured to be within AppData/Uploads inside the application's content root. This is our intended destination. Now, let's look at the Upload method.

// Upload method
public async Task<IActionResult> Upload(IFormFile file)
{
    if (file == null || file.Length == 0)
    {
        return BadRequest("You didn't provide a file.");
    }

    // The specific vulnerability: attacker can provide a file name with escape characters
    var fileName = file.FileName; // THIS IS THE VULNERABLE PART

    // Constructing the full path using the potentially malicious file name
    var filePath = Path.Combine(uploadRoute, fileName);

    // Writing the file to the constructed path
    using (var stream = new FileStream(filePath, FileMode.Create))
    {
        await file.CopyToAsync(stream);
    }

    return StatusCode(201); // Created
}

The critical vulnerability lies in how fileName is handled. The application directly uses file.FileName, which is the name provided by the client. If an attacker sends a filename like ../../../../src/Program.cs, the Path.Combine method, without proper sanitization, could construct a path that points outside the AppData/Uploads directory, potentially overwriting your Program.cs file.

Simulating the Attack with Fiddler

To truly understand the threat, we need to simulate it. A powerful tool for this is Fiddler. You can download it from telerik.com/fiddler. Fiddler is a web debugging proxy that lets you intercept and inspect HTTP traffic.

Here's how to set it up for our test:

  1. Launch Fiddler: Once installed, open Fiddler. It will start monitoring all web traffic on your computer.
  2. Configure Filters: To focus on your API, go to the Filters tab.
    • Enable Use Filters.
    • Under Host Filter, select Show only the following Hosts.
    • Enter localhost and the specific port your API is running on (e.g., localhost:51 if your app runs on port 51).
    • Click outside the text area to commit the change. This will significantly reduce the noise in your Fiddler pane.
  3. Set Breakpoints: To intercept requests before they reach your server, go to Rules > Automatic Breakpoints > Before Requests. Alternatively, you can press F11. This is crucial for modifying requests on the fly.

With Fiddler configured, you can launch your API. You might notice Swagger failing to load initially because Fiddler is intercepting requests. You can clear the breakpoint for Swagger to load, then re-enable it for your file upload test.

Now, let's try a normal file upload. We'll use a simple test.txt file. When you execute this request through Fiddler (after clearing the breakpoint temporarily for Swagger), you'll get a 201 Created response. If you check your project directory, you'll find a new AppData/Uploads folder containing your test.txt.

This confirms our upload endpoint is working. Now, for the attack.

  1. Clear Fiddler: Clear all existing requests in Fiddler.
  2. Re-enable Breakpoint: Ensure "Before Requests" is enabled.
  3. Send Malicious Request: Go back to your API client (e.g., Swagger UI), and this time, for the file name, enter something like ../../../../src/test.txt. This tells the system to go up two directories from AppData/Uploads, then up two more, and then into the src folder, attempting to place test.txt there.
  4. Intercept and Modify: When Fiddler intercepts this request, go to the Inspectors tab. You can view the raw request. Crucially, the filename field will contain your crafted path.
  5. Run to Completion: Click "Run to Completion" in Fiddler.

If successful, you'll still get a 201 Created response. However, if you check your project's src folder, you'll find test.txt there. This demonstrates that we've successfully traversed directories and placed a file outside of our intended upload location. This is a very real and dangerous attack.

Mitigating Path Traversal: Strategies and Best Practices

The core of the problem is trusting user-supplied input for file paths. To prevent path traversal, we need to implement robust validation and sanitization.

1. Principle of Least Privilege

This is a fundamental security concept. Your web application's hosting account (or app pool identity on Windows/IIS) should have write permissions only for the specific directories it absolutely needs. It should not have write access to the wwwroot directory, system paths, or any other sensitive areas. By limiting what the application can do, you limit the potential damage an attacker can cause.

2. Normalize and Sanitize Paths

Never directly use user-provided filenames in file system operations. Instead, follow these steps:

  • Sanitize the Filename: Remove or replace potentially dangerous characters. However, simply removing characters isn't always enough.
  • Generate Unique Filenames: The most secure approach is to discard the original filename entirely and generate a new, unique name for stored files. A GUID (Globally Unique Identifier) is an excellent choice for this. This completely decouples the stored filename from any user input.
  • Validate the Final Path: After constructing the intended save path, perform a final check to ensure it still resides within the allowed upload directory.

Let's refactor our Upload method to incorporate these fixes.

// In your FilesController

// Keep your existing uploadRoute setup:
// var uploadRoute = Path.Combine(_webHostEnvironment.ContentRootPath, "AppData", "Uploads");
// if (!Directory.Exists(uploadRoute)) { Directory.CreateDirectory(uploadRoute); }

public async Task<IActionResult> Upload(IFormFile file)
{
    if (file == null || file.Length == 0)
    {
        return BadRequest("You didn't provide a file.");
    }

    // 1. Generate a new unique filename using GUID
    var uniqueFileName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);

    // 2. Combine the upload directory with the new unique filename
    var filePath = Path.Combine(uploadRoute, uniqueFileName);

    // 3. **CRITICAL SAFETY CHECK:** Normalize and verify the final path
    // This ensures that even if something went wrong with GUID generation or Path.Combine,
    // we don't escape the intended upload directory.
    var normalizedPath = Path.GetFullPath(filePath); // Get the absolute path

    if (!normalizedPath.StartsWith(uploadRoute))
    {
        // Log the security breach and return a bad request
        // In a real app, you'd use a proper logging framework.
        Console.WriteLine($"SECURITY BREACH DETECTED: Attempted path traversal. Original path: {filePath}, Normalized path: {normalizedPath}");
        return BadRequest("Invalid file path.");
    }

    // Write the file using the safe, unique filename
    using (var stream = new FileStream(normalizedPath, FileMode.Create)) // Use normalizedPath here
    {
        await file.CopyToAsync(stream);
    }

    // Optionally, you might store the original file name in the database
    // associated with the uniqueFileName for display purposes later.
    // For example: await _dbContext.Files.AddAsync(new FileRecord { OriginalName = file.FileName, StoredName = uniqueFileName });
    // await _dbContext.SaveChangesAsync();

    return StatusCode(201); // Created
}

In this improved version:

  • We discard file.FileName and create uniqueFileName using Guid.NewGuid().ToString() combined with the original file's extension obtained via Path.GetExtension(file.FileName).
  • We then use Path.GetFullPath(filePath) to get the absolute, normalized path.
  • The crucial check !normalizedPath.StartsWith(uploadRoute) ensures that the resolved path is still a subdirectory of our intended uploadRoute. If it's not, we log it as a security breach and return an error.
  • Finally, we save the file using this normalizedPath.

3. Normalize Paths

The Path.GetFullPath() method is your friend here. It resolves relative paths, . (current directory), and .. (parent directory) components to produce a clean, absolute path. Combined with a StartsWith check against your base upload directory, it provides a strong defense.

Testing the Fix

After applying these changes, let's re-run our Fiddler test.

  1. Disable Breakpoints: Before sending your request, disable the breakpoints in Fiddler (Rules > Automatic Breakpoints > None, or press F11 again).
  2. Execute Upload: Submit the same malicious request with ../../../../src/test.txt as the filename.

This time, you should receive a BadRequest("Invalid file path.") response. If you check your file system, the test.txt file will not appear in the src directory. Instead, it will be saved securely within your AppData/Uploads folder with a GUID-based name. The security breach will be logged in your application's console output.

Conclusion: Security is an Ongoing Process

Path traversal is a serious vulnerability, but with proper input validation, sanitization, and adherence to the principle of least privilege, it's a threat you can effectively mitigate. By treating user input as untrusted and implementing checks such as generating unique filenames and verifying the final path, you build more resilient and secure applications. Remember, security isn't a one-time fix; it's a continuous process of understanding threats and implementing robust defenses.

Secure Coding in C# and ASP.NET

A comprehensive guide to building secure, compliant, and resilient software in the Microsoft .NET ecosystem. This course takes you far beyond theory—every module combines hands-on coding labs, real-world attack simulations, and AI-assisted security workflows using GitHub Copilot and modern .NET features.