flawopen.com/Path Traversal/C#

Path Traversal in C#

High Severity CWE-22
ELI5

You tell Windows to save a file to 'C:\App\Uploads'. The user enters '\Windows\System32\calc.exe'. Path.Combine treats the leading backslash as a command to start at drive root C:\, overwriting system files.

Key terms on this page
Path.GetFullPath
Normalizes the path by resolving '.' and '..' segments and canonicalizing slashes.
Path.DirectorySeparatorChar
Ensures boundary checks include the trailing slash to prevent sibling directory matches.

What's happening

In C# .NET, if the second argument of Path.Combine(basePath, userInput) starts with a directory separator (/ or \), Path.Combine discards basePath completely and returns the second argument relative to the drive root.

Real-world impact

In 2023, numerous .NET enterprise applications and file server APIs suffered arbitrary file download vulnerabilities due to leading-slash Gotchas in Path.Combine.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
// VULNERABLE: Path.Combine discards baseFolder if userInput has a leading slash
using System.IO;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class FileController : ControllerBase {
    private readonly string _baseFolder = @"C:\App\Storage\Uploads";

    [HttpGet("download")]
    public IActionResult Download([FromQuery] string file) {
        // If file is "\Windows\System32\cmd.exe", basePath is discarded!
        // If file is "..\..\App.config", it breaks out of the folder
        string targetPath = Path.Combine(_baseFolder, file);

        if (!System.IO.File.Exists(targetPath)) return NotFound();
        return PhysicalFile(targetPath, "application/octet-stream");
    }
}
FIXED
// HARDENED: Canonicalize path and assert directory prefix containment
using System;
using System.IO;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class FileController : ControllerBase {
    private readonly string _baseFolder;

    public FileController() {
        _baseFolder = Path.GetFullPath(@"C:\App\Storage\Uploads");
    }

    [HttpGet("download")]
    public IActionResult Download([FromQuery] string file) {
        if (string.IsNullOrWhiteSpace(file)) return BadRequest("Invalid filename");

        // 1. Strip leading slashes to prevent Path.Combine root discard
        string cleanName = file.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);

        // 2. Resolve canonical full path
        string fullPath = Path.GetFullPath(Path.Combine(_baseFolder, cleanName));

        // 3. Strict boundary containment check with DirectorySeparatorChar
        string safePrefix = _baseFolder.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
        if (!fullPath.StartsWith(safePrefix, StringComparison.OrdinalIgnoreCase)) {
            return Forbid("Path traversal attempt detected");
        }

        if (!System.IO.File.Exists(fullPath)) return NotFound();
        return PhysicalFile(fullPath, "application/octet-stream");
    }
}

Why the fix works

Path.GetFullPath() collapses all relative dots. Checking fullPath.StartsWith(safePrefix, StringComparison.OrdinalIgnoreCase) verifies the target is inside the base folder and accounts for Windows case insensitivity.

Gotchas

Path.Combine leading slash gotcha

Path.Combine('C:\\app', '\\temp') yields 'C:\\temp', not 'C:\\app\\temp'. Always trim leading slashes before combining.

Common misconceptions

"Path.GetFileName() is always sufficient"

Path.GetFileName() strips subdirectories completely, preventing legitimate multi-level folder structures in an app.

How to check if you're affected

dotnet format analyzers --severity warn # Roslyn Rule CA3003

Prevention checklist

FAQ

Why StringComparison.OrdinalIgnoreCase in C#?

On Windows, paths are case-insensitive (c:\app == C:\APP). Using default ordinal comparison could allow traversal bypasses via case variations.

References