CWE-73: External Control of File Name or Path - C#
Overview
External control of file names or paths is a critical vulnerability in C#/.NET applications that occurs when user-supplied input is used to construct file system paths without proper validation. The .NET framework provides powerful file I/O capabilities through System.IO, but offers minimal built-in protection against path traversal attacks.
Primary Defence: Use Path.GetFullPath() with StartsWith() validation to ensure resolved paths stay within the intended base directory, implement allowlists for known file sets, use GUID-based indirect reference maps for sensitive file access, and sanitize uploaded filenames with Path.GetFileName() combined with extension validation to prevent path traversal, absolute path injection, and symlink attacks.
Remediation Steps
Locate the Finding
Identify the Source (User Input)
- Look for where untrusted data enters the application:
- HTTP parameters:
Request.Query["filename"],Request.Form["path"] - Route parameters:
[FromRoute],[FromQuery],[FromBody]attributes - File uploads:
IFormFile.FileNameproperty - Headers:
Request.Headers["X-File-Path"] - JSON/XML input: Deserialized objects with file path properties
- HTTP parameters:
Identify the Sink (File Operation)
- Trace to where the data reaches file system operations:
File.ReadAllBytes(),File.ReadAllText(),File.WriteAllText()FileStreamconstructorStreamReader,StreamWriterconstructorsFileInfo,DirectoryInfooperationsPath.Combine(),Path.GetFullPath()
Example from Security Scan Finding:
// Source: User-controlled filename from query parameter
string filename = Request.Query["file"]; // ← SOURCE
// Sink: File operation using untrusted input
var content = File.ReadAllBytes(filename); // ← SINK
Understand the Data Flow
Review the data flow in the security finding:
- Entry Point: Where user input enters (controller action, API endpoint)
- Intermediate Processing: Any transformations or validations (often insufficient)
- File Operation: The vulnerable sink where file access occurs
Key Questions:
- Is there any validation between source and sink?
- Are there string operations that might be bypassable? (
.Contains(".."),.Replace()) - Does the path go through
Path.Combine()without validation? - Is there any canonicalization with
Path.GetFullPath()?
Common Data Flow Pattern:
[HttpGet("download")] // ← Entry point
public IActionResult Download(string file) // ← Source
{
// Insufficient validation (bypassable)
if (file.Contains(".."))
return BadRequest();
var path = Path.Combine(@"C:\uploads", file); // ← Transformation
var bytes = File.ReadAllBytes(path); // ← Sink
return File(bytes, "application/octet-stream");
}
Identify the Pattern
Match the code to vulnerable patterns:
- Absolute path injection → Pattern:
Path.Combine(@"C:\data", userInput)where userInput =@"C:\Windows\win.ini" - Directory traversal → Pattern:
Path.Combine(@"C:\uploads", @"..\..\appsettings.json") - Unsafe filename from upload → Pattern:
file.CopyTo(new FileStream(file.FileName, FileMode.Create)) - String concatenation → Pattern:
@"C:\data\" + userInputinstead ofPath.Combine() - Denylist bypass → Pattern:
if (path.Contains(".."))(bypassable with URL encoding:%2e%2e) - Not using Path.GetFullPath → Pattern: File operations without canonicalization
Verify the Fix
Verification checklist:
Test path traversal attacks
Ensure directory traversal patterns are rejected
# Try these patterns - all should fail with 400/403/404:
?file=..\..\appsettings.json
?file=..\..\..\Windows\System32\config\SAM
?file=..%5c..%5csensitive.txt (URL-encoded backslash)
?file=....\\....\\sensitive.txt (Double-dot)
Test absolute path injection
Verify absolute paths don't override base directory
?file=C:\Windows\win.ini
?file=C:\Program Files\app\config.ini
\\?\C:\Windows\System32\config\SAM (Windows extended path)
Test UNC paths
Reject network paths
Test file upload paths
Upload files with malicious names should be rejected or sanitized
Upload filename: ..\..\wwwroot\shell.aspx
Upload filename: /etc/passwd
Upload filename: ..\..\.htaccess
Verify legitimate access
Confirm allowed files are still accessible
Unit test validation
(optional - adapt to your test framework)
- Test validator rejects all traversal patterns
- Test validator rejects absolute paths
- Test validator accepts valid relative paths within base directory
- Test validator correctly canonicalizes paths with
Path.GetFullPath() - Verify
StartsWith()comparison works correctly
Check for Similar Issues
Search your codebase for other vulnerable patterns:
Visual Studio / VS Code Search Patterns:
File\.Read
File\.Write
FileStream
StreamReader
StreamWriter
Path\.Combine
IFormFile\.FileName
Request\.Query.*file
Request\.Form.*path
Common locations to check:
- All controller actions with file parameters
- File upload handlers
- Static file middleware configuration
- Document download endpoints
- Report generation that saves files
- Backup/restore functionality
- Log file access features
- Template or theme file handling
Review code patterns like:
// Pattern 1: Direct file parameter usage
public IActionResult Download(string file)
{
return File(System.IO.File.ReadAllBytes(file), ...);
}
// Pattern 2: Path concatenation
var path = baseDir + "\\" + userInput;
// Pattern 3: Insufficient validation
if (filename.Contains("..")) { /* reject */ }
// Pattern 4: Using original upload filename
file.CopyTo(new FileStream(file.FileName, FileMode.Create));
ASP.NET Core specific areas:
StaticFileOptionsconfiguration- Custom middleware handling file requests
- Blazor file upload components
- SignalR file transfer implementations
- Areas using
PhysicalFileProvider
Common Vulnerable Patterns
Direct Use of User Input in File Operations
// VULNERABLE - No validation of user-supplied filename
[HttpGet("download")]
public IActionResult DownloadFile(string filename)
{
// User can provide any path
var content = System.IO.File.ReadAllBytes(filename);
return File(content, "application/octet-stream");
}
// Attack example:
// GET /download?filename=C:\Windows\System32\config\SAM
// GET /download?filename=../../../appsettings.json
// Result: Reads sensitive files from the server
Insufficient String-Based Validation
// VULNERABLE - Denylist can be bypassed
[HttpGet("read")]
public IActionResult ReadFile(string filename)
{
// Incomplete validation
if (filename.Contains(".."))
{
return BadRequest("Invalid filename");
}
var path = Path.Combine(@"C:\app\data", filename);
var content = System.IO.File.ReadAllText(path);
return Content(content);
}
// Attack example:
// GET /read?filename=..%5C..%5C..%5Cappsettings.json
// Result: URL-encoded backslash bypasses the check
Not Using Path.GetFullPath for Validation
// VULNERABLE - Path.Combine doesn't prevent absolute paths
[HttpGet("file/{*filename}")]
public IActionResult GetFile(string filename)
{
// Path.Combine allows absolute paths if second arg is absolute
var fullPath = Path.Combine(@"C:\app\data", filename);
var content = System.IO.File.ReadAllBytes(fullPath);
return File(content, "application/octet-stream");
}
// Attack example:
// GET /file/C:\Windows\win.ini
// Result: Path.Combine(@"C:\app\data", @"C:\Windows\win.ini") = @"C:\Windows\win.ini"
Trusting IFormFile.FileName Without Sanitization
// VULNERABLE - Using original filename without proper validation
[HttpPost("upload")]
public async Task<IActionResult> UploadFile(IFormFile file)
{
if (file == null || file.Length == 0)
return BadRequest("No file uploaded");
// Path.GetFileName helps but isn't sufficient
var filename = Path.GetFileName(file.FileName);
var path = Path.Combine(@"C:\uploads", filename);
using (var stream = new FileStream(path, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return Ok();
}
// Attack example:
// Upload file with name: ..\..\wwwroot\malicious.html
// Still vulnerable to various encoding and Unicode attacks
Using StreamReader/StreamWriter with User Input
// VULNERABLE - Direct string concatenation for paths
[HttpDelete("delete")]
public IActionResult DeleteFile(string filename)
{
var filepath = @"C:\app\temp\" + filename;
System.IO.File.Delete(filepath);
return Ok();
}
// Attack example:
// DELETE /delete?filename=..\..\important.dll
// Result: Deletes C:\important.dll
Secure Patterns
Allowlist with Path Validation (Most Secure)
public class SecureFileService
{
private readonly string _baseDirectory;
private readonly HashSet<string> _allowedFiles;
public SecureFileService(string baseDirectory)
{
_baseDirectory = Path.GetFullPath(baseDirectory);
_allowedFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
"report.pdf",
"summary.txt",
"data.csv"
};
}
public byte[] ReadFile(string filename)
{
if (!_allowedFiles.Contains(filename))
throw new UnauthorizedAccessException("File not allowed");
var filePath = Path.GetFullPath(Path.Combine(_baseDirectory, filename));
// Robust containment: compute relative path
var relative = Path.GetRelativePath(_baseDirectory, filePath);
if (relative == ".." || relative.StartsWith(".." + Path.DirectorySeparatorChar))
throw new UnauthorizedAccessException("Path traversal detected");
if (!File.Exists(filePath))
throw new FileNotFoundException("File not found", filename);
return File.ReadAllBytes(filePath);
}
}
// Usage:
var service = new SecureFileService(@"C:\app\data");
var content = service.ReadFile(userInput);
Why this works:
- Access is restricted to an exact allowlist of approved filenames.
- The target path is canonicalized with
Path.GetFullPath()before validation. - Containment is enforced using a relative-path check (
Path.GetRelativePath), avoiding string-prefix bypasses. - With appropriate filesystem permissions (base directory not attacker-writable), this prevents path traversal and common path injection attempts.
Path Canonicalization with Ancestor Validation (Flexible and Secure)
public class SecurePathValidator
{
private readonly string _baseDirectory;
public SecurePathValidator(string baseDirectory)
{
// Get absolute, normalized base directory path
_baseDirectory = Path.GetFullPath(baseDirectory);
if (!Directory.Exists(_baseDirectory))
{
throw new DirectoryNotFoundException(
$"Base directory not found: {baseDirectory}");
}
}
public string ValidatePath(string userPath)
{
if (string.IsNullOrWhiteSpace(userPath))
throw new ArgumentException("Path cannot be empty", nameof(userPath));
var full = Path.GetFullPath(Path.Combine(_baseDirectory, userPath));
var rel = Path.GetRelativePath(_baseDirectory, full);
if (rel == ".." || rel.StartsWith(".." + Path.DirectorySeparatorChar))
throw new UnauthorizedAccessException($"Path traversal detected: {userPath}");
return full;
}
}
// Usage:
var validator = new SecurePathValidator(@"C:\app\data");
var safePath = validator.ValidatePath(userInput);
var content = File.ReadAllText(safePath);
Why this works:
Path.GetFullPath()collapses./..and normalizes an absolute path before validation.- Containment is enforced with
Path.GetRelativePath(), avoiding string-prefix bypasses (e.g.,C:\base_evil). - This prevents directory traversal and absolute-path injection by ensuring the final path remains under the trusted base directory.
- If the base directory is attacker-writable, additional defenses may be needed against symlink/junction (reparse point) attacks.
GUID-Based Indirect References
public class SecureFileRegistry
{
private readonly string _baseDirectory;
private readonly System.Collections.Concurrent.ConcurrentDictionary<Guid, string> _registry;
public SecureFileRegistry(string baseDirectory)
{
_baseDirectory = Path.GetFullPath(baseDirectory);
_registry = new System.Collections.Concurrent.ConcurrentDictionary<Guid, string>();
}
public Guid RegisterFile(string internalPath)
{
var full = Path.GetFullPath(Path.Combine(_baseDirectory, internalPath));
var rel = Path.GetRelativePath(_baseDirectory, full);
if (rel == ".." || rel.StartsWith(".." + Path.DirectorySeparatorChar))
throw new UnauthorizedAccessException($"Invalid file path: {internalPath}");
if (!File.Exists(full))
throw new FileNotFoundException($"File not found: {internalPath}");
var token = Guid.NewGuid();
_registry[token] = full;
return token;
}
public byte[] GetFile(Guid token)
{
if (!_registry.TryGetValue(token, out var full))
throw new FileNotFoundException("Invalid file token");
return File.ReadAllBytes(full);
}
}
// Usage:
var registry = new SecureFileRegistry(@"C:\app\data");
var token = registry.RegisterFile(@"reports\2024\q1.pdf");
// Return token to user, they can only access via this token
var content = registry.GetFile(token);
Why this works:
- Users receive opaque GUID tokens instead of filesystem paths.
- The server resolves and validates requested paths under a trusted base directory before registering them.
- After registration, file access uses the server-side mapping, so user input cannot influence path resolution at read time.
- Tokens should be scoped (user/tenant) and time-limited to reduce the impact of token leakage; GUIDs alone are not authorization.
Secure Filename Sanitization
// Sanitizes and validates filenames
public class SecureFilenameHandler
{
private static readonly HashSet<string> AllowedExtensions =
new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
".pdf", ".txt", ".csv", ".xlsx"
};
private static readonly char[] InvalidChars =
Path.GetInvalidFileNameChars();
public static string SanitizeFilename(string filename)
{
if (string.IsNullOrWhiteSpace(filename))
{
throw new ArgumentException(
"Filename cannot be empty", nameof(filename));
}
// Get just the filename, removing any path components
var cleanName = Path.GetFileName(filename);
if (string.IsNullOrWhiteSpace(cleanName))
{
throw new ArgumentException(
"Invalid filename", nameof(filename));
}
// Remove invalid characters
var sanitized = string.Join("_",
cleanName.Split(InvalidChars, StringSplitOptions.RemoveEmptyEntries));
// Validate extension
var extension = Path.GetExtension(sanitized);
if (!AllowedExtensions.Contains(extension))
{
throw new ArgumentException(
$"File type not allowed: {extension}");
}
return sanitized;
}
}
// Usage:
var safeFilename = SecureFilenameHandler.SanitizeFilename(userInput);
var filePath = Path.Combine(@"C:\uploads", safeFilename);
Why this works (and what it doesn’t do):
Path.GetFileName()discards any directory components so the result is a simple filename.- Invalid characters are removed to produce an OS-compatible name.
- Extension allowlisting enforces a restricted set of accepted filename suffixes.
- This is filename hygiene only; you must still enforce real-path containment and safe file handling (e.g., prevent symlink/junction escapes and overwrites) when opening or writing files.
Framework-Specific Guidance
ASP.NET Core - Secure File Upload and Download
// ASP.NET Core upload/download with robust path checks + streaming
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System.Text.RegularExpressions;
[ApiController]
[Route("api/files")]
public class SecureFileController : ControllerBase
{
private readonly string _uploadBase; // canonical full path
private readonly long _maxFileSize = 10 * 1024 * 1024; // 10MB
private readonly ILogger<SecureFileController> _logger;
private static readonly HashSet<string> AllowedExtensions =
new(StringComparer.OrdinalIgnoreCase) { ".pdf", ".txt", ".csv", ".xlsx" };
private static readonly HashSet<string> AllowedContentTypes =
new(StringComparer.OrdinalIgnoreCase)
{
"application/pdf",
"text/plain",
"text/csv",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
};
private static readonly Regex DownloadNamePattern =
new(@"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\.(pdf|txt|csv|xlsx)$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
public SecureFileController(IWebHostEnvironment env, ILogger<SecureFileController> logger)
{
_logger = logger;
var uploadPath = Path.Combine(env.ContentRootPath, "uploads");
Directory.CreateDirectory(uploadPath);
// Canonicalize once
_uploadBase = Path.GetFullPath(uploadPath);
}
[HttpPost("upload")]
[RequestSizeLimit(10_000_000)]
public async Task<IActionResult> UploadFile(IFormFile file, CancellationToken ct)
{
if (file == null || file.Length <= 0)
return BadRequest("No file provided");
if (file.Length > _maxFileSize)
return BadRequest("File too large");
// Advisory/policy check only (not a file-signature guarantee)
if (string.IsNullOrWhiteSpace(file.ContentType) || !AllowedContentTypes.Contains(file.ContentType))
return BadRequest($"File type not allowed: {file.ContentType}");
var originalName = Path.GetFileName(file.FileName);
if (string.IsNullOrWhiteSpace(originalName))
return BadRequest("Invalid filename");
var ext = Path.GetExtension(originalName);
if (string.IsNullOrWhiteSpace(ext) || !AllowedExtensions.Contains(ext))
return BadRequest($"File extension not allowed: {ext}");
// Server-generated storage name (prevents collisions/path injection)
var storedName = $"{Guid.NewGuid()}{ext.ToLowerInvariant()}";
// Build + canonicalize full target path
var targetFullPath = Path.GetFullPath(Path.Combine(_uploadBase, storedName));
// Robust containment check (avoids string-prefix bypass)
var rel = Path.GetRelativePath(_uploadBase, targetFullPath);
if (rel == ".." || rel.StartsWith(".." + Path.DirectorySeparatorChar))
return BadRequest("Invalid file path");
try
{
// CreateNew avoids overwriting anything unexpectedly
await using var fs = new FileStream(
targetFullPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
bufferSize: 81920,
useAsync: true
);
await file.CopyToAsync(fs, ct);
return Ok(new { filename = storedName });
}
catch (IOException ioEx)
{
// Handle rare collisions / filesystem issues without leaking details
_logger.LogWarning(ioEx, "Upload failed for {StoredName}", storedName);
return StatusCode(500, "Upload failed");
}
catch (OperationCanceledException)
{
return StatusCode(499); // client closed request (commonly used)
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected upload failure");
return StatusCode(500, "Upload failed");
}
}
[HttpGet("download/{filename}")]
public IActionResult DownloadFile(string filename)
{
if (string.IsNullOrWhiteSpace(filename) || !DownloadNamePattern.IsMatch(filename))
return BadRequest("Invalid filename format");
// TODO: enforce authorization here (owner/role/tenant checks).
// e.g., validate the requesting user is allowed to access this filename/token.
var fullPath = Path.GetFullPath(Path.Combine(_uploadBase, filename));
var rel = Path.GetRelativePath(_uploadBase, fullPath);
if (rel == ".." || rel.StartsWith(".." + Path.DirectorySeparatorChar))
return BadRequest("Invalid file path");
if (!System.IO.File.Exists(fullPath))
return NotFound("File not found")
Why this works:
- Files are stored under a fixed server-controlled directory using server-generated GUID filenames (no user-controlled paths).
- Extensions are allowlisted and downloads only accept a strict GUID+extension format, reducing path manipulation and guessing.
- Paths are canonicalized with
Path.GetFullPath()and validated for containment using a relative-path check, avoiding string-prefix bypasses. - Upload size limits reduce resource-exhaustion risk; content-type checks are an additional policy filter (not a strong type guarantee).
- Downloads should still enforce authorization; unguessable filenames reduce risk but are not access control.
ASP.NET Core - Static File Serving Configuration
using Microsoft.Extensions.FileProviders;
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(env.ContentRootPath, "wwwroot")),
RequestPath = "/static",
ServeUnknownFileTypes = false,
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers["X-Content-Type-Options"] = "nosniff";
// Optional: cache static assets
// ctx.Context.Response.Headers["Cache-Control"] = "public,max-age=31536000,immutable";
}
});
app.UseRouting();
app.UseEndpoints(endpoints => endpoints.MapControllers());
}
}
Why this works:
- Static content is served only from a fixed directory (
wwwroot) via aPhysicalFileProvider, reducing the chance of exposing application files. - Requests are scoped to a dedicated URL prefix (
/static), separating asset URLs from application routes. - Unknown file types are not served (
ServeUnknownFileTypes = false), reducing exposure of unexpected file extensions. X-Content-Type-Options: nosniffhelps prevent browsers from MIME-sniffing responses into executable types.- Directory browsing is disabled when using
UseFileServer(static files alone do not enable directory listings).
Entity Framework Core - Secure File Metadata Storage
using Microsoft.EntityFrameworkCore;
public class FileMetadata
{
public Guid Id { get; set; }
public string OriginalFilename { get; set; } = "";
public string StoredFilename { get; set; } = "";
public string ContentType { get; set; } = "";
public long Size { get; set; }
public DateTime UploadedAt { get; set; }
public string UploadedBy { get; set; } = "";
}
public class SecureFileRepository
{
private static readonly HashSet<string> AllowedExtensions =
new(StringComparer.OrdinalIgnoreCase) { ".pdf", ".txt", ".csv", ".xlsx" };
private readonly ApplicationDbContext _context;
private readonly string _storageBase; // canonical
private readonly string _storageBaseWithSep;
public SecureFileRepository(ApplicationDbContext context, string storagePath)
{
_context = context;
_storageBase = Path.GetFullPath(storagePath);
Directory.CreateDirectory(_storageBase);
_storageBaseWithSep = _storageBase.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
}
public async Task<FileMetadata> StoreFileAsync(
string originalFilename,
Stream fileStream,
string contentType,
string userId,
CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(userId))
throw new UnauthorizedAccessException("Missing user context");
var safeOriginal = Path.GetFileName(originalFilename ?? "");
if (string.IsNullOrWhiteSpace(safeOriginal))
throw new ArgumentException("Invalid filename", nameof(originalFilename));
var ext = Path.GetExtension(safeOriginal);
if (string.IsNullOrWhiteSpace(ext) || !AllowedExtensions.Contains(ext))
throw new ArgumentException($"File extension not allowed: {ext}");
// Server-controlled stored name
var storedFilename = $"{Guid.NewGuid():D}{ext.ToLowerInvariant()}";
var fullPath = Path.GetFullPath(Path.Combine(_storageBase, storedFilename));
// Robust containment (avoids prefix bypass)
var rel = Path.GetRelativePath(_storageBase, fullPath);
if (rel == ".." || rel.StartsWith(".." + Path.DirectorySeparatorChar))
throw new UnauthorizedAccessException("Invalid storage path");
try
{
await using var outStream = new FileStream(
fullPath,
FileMode.CreateNew,
FileAccess.Write,
FileShare.None,
bufferSize: 81920,
useAsync: true);
await fileStream.CopyToAsync(outStream, ct);
}
catch
{
// Best-effort cleanup
if (System.IO.File.Exists(fullPath))
System.IO.File.Delete(fullPath);
throw;
}
var size = new FileInfo(fullPath).Length;
var metadata = new FileMetadata
{
Id = Guid.NewGuid(),
OriginalFilename = safeOriginal,
StoredFilename = storedFilename,
ContentType = contentType ?? "application/octet-stream", // advisory
Size = size,
UploadedAt = DateTime.UtcNow,
UploadedBy = userId
};
_context.Files.Add(metadata);
await _context.SaveChangesAsync(ct);
return metadata;
}
public async Task<(string fullPath, string downloadName, string contentType)> GetFilePathAsync(
Guid fileId,
string userId,
CancellationToken ct = default)
{
var metadata = await _context.Files.AsNoTracking().FirstOrDefaultAsync(f => f.Id == fileId, ct);
if (metadata == null)
throw new FileNotFoundException("File not found");
// Authorization example: owner-only
if (!string.Equals(metadata.UploadedBy, userId, StringComparison.Ordinal))
throw new UnauthorizedAccessException("Access denied");
var fullPath = Path.GetFullPath(Path.Combine(_storageBase, metadata.StoredFilename));
var rel = Path.GetRelativePath(_storageBase, fullPath);
if (rel == ".." || rel.StartsWith(".." + Path.DirectorySeparatorChar))
throw new UnauthorizedAccessException("Invalid file path");
if (!System.IO.File.Exists(fullPath))
throw new FileNotFoundException("File not found");
return (fullPath, metadata.OriginalFilename, metadata.ContentType);
}
}
Why this works:
- Files are stored using server-generated GUID filenames, so user input never becomes a filesystem path.
- The database is the authoritative mapping from file IDs to stored filenames (indirect reference pattern).
- Original filenames are kept only for display/download names and are reduced to basenames with
Path.GetFileName(). - Paths are canonicalized and validated for containment using a relative-path check, avoiding string-prefix bypasses.
- Authorization can be enforced at the metadata layer (e.g.,
UploadedBy) before any filesystem access.
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced