flawopen.com/Path Traversal/C#
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.
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.
In 2023, numerous .NET enterprise applications and file server APIs suffered arbitrary file download vulnerabilities due to leading-slash Gotchas in Path.Combine.
// 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");
}
}
// 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");
}
}
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.
Path.Combine('C:\\app', '\\temp') yields 'C:\\temp', not 'C:\\app\\temp'. Always trim leading slashes before combining.
Path.GetFileName() strips subdirectories completely, preventing legitimate multi-level folder structures in an app.
dotnet format analyzers --severity warn
# Roslyn Rule CA3003
Path.Combine()Path.GetFullPath() to resolve traversal dotsfullPath.StartsWith(safePrefix, StringComparison.OrdinalIgnoreCase)ZipFileEntry.FullName boundary checksOn Windows, paths are case-insensitive (c:\app == C:\APP). Using default ordinal comparison could allow traversal bypasses via case variations.