CWE-377: Insecure Temporary File - C#
Overview
Insecure temporary file creation in C# occurs when applications create files with predictable names, insecure permissions, or without proper cleanup. The .NET framework provides Path.GetTempFileName(), Path.GetTempPath(), and modern file options like FileOptions.DeleteOnClose that should be used for secure temporary file handling.
Primary Defence: Use Path.GetTempFileName() for secure unpredictable filenames, FileOptions.DeleteOnClose for automatic cleanup, and set ACLs for restrictive permissions.
Common Vulnerable Patterns
Predictable filename in temp directory
using System;
using System.IO;
// VULNERABLE - Predictable filename using PID
public class InsecureTemp
{
public void SaveUserData(string userData)
{
int pid = Environment.ProcessId; // .NET 5+
string tempFile = $@"C:\Temp\userdata_{pid}.txt";
// Attackers can predict the PID
File.WriteAllText(tempFile, userData);
ProcessFile(tempFile);
// File not deleted - persists in temp directory
}
}
Fixed filename in shared directory
using System.IO;
// VULNERABLE - Fixed filename, race condition
public void ExportCredentials(string apiKey, string secret)
{
string tempFile = @"C:\Temp\credentials.txt";
// Multiple processes might use same filename
// Default ACL may allow other users to read
string data = $"API_KEY={apiKey}\nSECRET={secret}\n";
File.WriteAllText(tempFile, data);
// File not cleaned up
}
Using timestamp for filename
using System;
using System.IO;
// VULNERABLE - Timestamp-based filename is predictable
public void CreateTempLog()
{
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
string tempFile = $@"C:\Temp\log_{timestamp}.txt";
// Attacker can predict the timestamp
File.WriteAllText(tempFile, "Sensitive log data");
}
Not setting restrictive permissions
using System.IO;
// VULNERABLE - Default Windows permissions may be too permissive
public void SaveSensitiveData(string data)
{
string tempFile = Path.Combine(Path.GetTempPath(), "sensitive.txt");
// File inherits parent directory permissions
// May be readable by other users on the system
File.WriteAllText(tempFile, data);
}
Not cleaning up temporary files
using System;
using System.IO;
// VULNERABLE - Temp files accumulate
public string ProcessSensitiveData(string data)
{
var random = new Random();
string tempFile = Path.Combine(Path.GetTempPath(), $"data_{random.Next(10000)}.tmp");
File.WriteAllText(tempFile, data);
string result = Analyze(tempFile);
// File never deleted - sensitive data persists
return result;
}
Using Path.GetTempFileName (predictable)
using System.IO;
// VULNERABLE - Only 65,536 possible filenames (tmpXXXX.tmp)
// Predictable pattern enables enumeration attacks
public void ProcessData(string data)
{
string tempFile = Path.GetTempFileName();
try
{
File.WriteAllText(tempFile, data);
ProcessFile(tempFile);
}
finally
{
File.Delete(tempFile);
}
}
Secure Patterns
Using Path.GetRandomFileName (cross-platform approach)
using System;
using System.IO;
using System.Runtime.InteropServices;
public class SecureTemp
{
public void ProcessSecureData(string data)
{
// GetRandomFileName generates cryptographically random 11-character name
// Does NOT create the file - you must create it with FileMode.CreateNew
string tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
try
{
// Create file with CreateNew (fails if exists - prevents race conditions)
using (var stream = new FileStream(
tempFile,
FileMode.CreateNew, // Atomic creation
FileAccess.ReadWrite,
FileShare.None,
4096,
FileOptions.None))
{
byte[] dataBytes = System.Text.Encoding.UTF8.GetBytes(data);
stream.Write(dataBytes, 0, dataBytes.Length);
}
// Set restrictive permissions (cross-platform)
SetFilePermissionsOwnerOnly(tempFile);
// Process the file - replace ProcessFile() with your actual temp file operations
// (parsing, validation, transformation, etc.)
ProcessFile(tempFile);
}
finally
{
// Always delete the temp file
if (File.Exists(tempFile))
{
File.Delete(tempFile);
}
}
}
private void SetFilePermissionsOwnerOnly(string path)
{
#if NET6_0_OR_GREATER
// On Unix systems, set file mode to 0600 (owner read/write only)
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite);
}
else
#endif
{
// Windows - use ACL
var fileInfo = new FileInfo(path);
var fileSecurity = fileInfo.GetAccessControl();
// Remove inherited permissions
fileSecurity.SetAccessRuleProtection(true, false);
// Add owner-only permissions
var currentUser = System.Security.Principal.WindowsIdentity.GetCurrent();
var rule = new System.Security.AccessControl.FileSystemAccessRule(
currentUser.User,
System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.AccessControlType.Allow
);
fileSecurity.SetAccessRule(rule);
fileInfo.SetAccessControl(fileSecurity);
}
}
}
Why this works:
- Cryptographically random filenames:
Path.GetRandomFileName()generates 11-character names with ~2^66 possible combinations (8 alphanumeric chars + dot + 3-char extension) - Superior to predictable approaches: Timestamps are guessable within seconds, PIDs have limited range (~32,000 values),
new Random()is pseudorandom and predictable with known seed - Better than Path.GetTempFileName(): That method only provides 65,536 possibilities (tmpXXXX.tmp pattern) and automatically creates the file
- Name-only generation: Does not automatically create the file, giving you control over creation flags
- Atomic file creation:
FileMode.CreateNewfails if file exists, preventing race conditions where attackers pre-create files with known names - Cross-platform permission handling:
- .NET 6+ Unix:
File.SetUnixFileMode()sets0600permissions (owner read/write only) - Windows (or pre-.NET 6): ACLs explicitly grant access only to current user, removing inherited permissions
- Prevents Unix umask default (
0022) creating world-readable0644files
- .NET 6+ Unix:
- Guaranteed cleanup: Try-finally ensures file deletion even on exceptions
When to use: Best default choice for most applications - built-in .NET method, strong randomness, cross-platform secure, works on all .NET versions. Choose this over manual RandomNumberGenerator implementations for simplicity, or upgrade to SecureTempFileManager for maximum security (2^128 entropy) when handling highly sensitive data.
Using FileOptions.DeleteOnClose
using System;
using System.IO;
void ProcessWithAutoDelete(string data)
{
string tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
using var stream = new FileStream(
tempFile,
FileMode.CreateNew,
FileAccess.ReadWrite,
FileShare.None,
4096,
FileOptions.DeleteOnClose);
using (var writer = new StreamWriter(stream, System.Text.Encoding.UTF8, bufferSize: 1024, leaveOpen: true))
{
writer.Write(data);
writer.Flush();
}
// do other stuff while stream is still open
stream.Position = 0;
using var reader = new StreamReader(stream, System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true);
var roundTrip = reader.ReadToEnd();
}
Why this works:
- OS-level automatic deletion:
FileOptions.DeleteOnCloseinstructs the OS to delete the file when the last handle closes - works even if application crashes - No explicit Delete() needed: OS handles cleanup automatically, providing guaranteed deletion
- Race condition prevention:
FileMode.CreateNewwith random filename fails if file exists - Exception-safe cleanup:
usingstatement ensures stream disposal (and file deletion) even on exceptions - Minimal data exposure: File deleted immediately when closed, reducing sensitive data lifetime
- Default security: Windows typically makes these files user-only accessible (explicit ACL setting still recommended for sensitive data)
When to use this pattern:
- Processing temp file data entirely through streams (no need to pass file path to external tools)
- Wanting OS-level guaranteed deletion even if process crashes mid-operation
- Working with highly sensitive data that should exist for shortest possible time
Avoid this pattern when:
- Need to pass file path to external processes/tools that open their own handles (file deleted when your stream closes, potentially before external tool finishes)
- Need to keep file accessible after initial processing (use regular
Path.GetRandomFileName()with manual cleanup instead)
Using cryptographically random temp file names (RECOMMENDED)
using System;
using System.IO;
using System.Security.Cryptography;
public class SecureTempFileManager
{
public string CreateSecureTempFile(string prefix = "secure_", string suffix = ".tmp")
{
string tempDir = Path.GetTempPath();
// Generate cryptographically random filename (2^128 possibilities - unpredictable)
// PREFERRED over Path.GetTempFileName() for sensitive data
string randomName = GenerateRandomFileName();
string tempFile = Path.Combine(tempDir, $"{prefix}{randomName}{suffix}");
// Create file atomically
using (var stream = new FileStream(
tempFile,
FileMode.CreateNew, // Fail if exists (prevents race conditions)
FileAccess.ReadWrite,
FileShare.None,
4096,
FileOptions.None))
{
// File created
}
// Set restrictive permissions
SetOwnerOnlyPermissions(tempFile);
return tempFile;
}
private string GenerateRandomFileName()
{
byte[] randomBytes = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomBytes);
}
return BitConverter.ToString(randomBytes).Replace("-", "").ToLower();
}
private void SetOwnerOnlyPermissions(string path)
{
var fileInfo = new FileInfo(path);
var fileSecurity = fileInfo.GetAccessControl();
// Disable inheritance
fileSecurity.SetAccessRuleProtection(true, false);
// Remove all existing rules
foreach (System.Security.AccessControl.FileSystemAccessRule rule in
fileSecurity.GetAccessRules(true, false, typeof(System.Security.Principal.SecurityIdentifier)))
{
fileSecurity.RemoveAccessRule(rule);
}
// Add owner-only permissions
var currentUser = System.Security.Principal.WindowsIdentity.GetCurrent();
var ownerRule = new System.Security.AccessControl.FileSystemAccessRule(
currentUser.User,
System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.AccessControlType.Allow
);
fileSecurity.SetAccessRule(ownerRule);
fileInfo.SetAccessControl(fileSecurity);
}
}
// Usage
var manager = new SecureTempFileManager();
string tempFile = manager.CreateSecureTempFile();
try
{
File.WriteAllText(tempFile, sensitiveData);
ProcessFile(tempFile);
}
finally
{
File.Delete(tempFile);
}
Why this works:
- Maximum cryptographic randomness:
RandomNumberGenerator.Create()with 16 bytes generates 128-bit random values, providing 2^128 (340 undecillion) possible filenames - Vastly superior to alternatives:
Path.GetTempFileName()only provides 65,536 possibilities - this makes filename prediction computationally infeasible - Atomic file creation:
FileMode.CreateNewfails if file exists, preventing race conditions - Explicit permission control: Custom
SetOwnerOnlyPermissions()method removes all inherited permissions and grants access only to current user - Stronger than defaults: Provides better isolation than default Windows permissions
- Secure before data written: File created empty with minimal permissions before any sensitive data is written
- Enterprise-grade security: Combination of unpredictable names, atomic creation, explicit permissions, and guaranteed cleanup
When to use: Maximum security scenarios - handling highly sensitive data, compliance requirements (HIPAA, PCI-DSS), enterprise applications requiring strongest possible temp file protection.
Temporary file wrapper class with IDisposable
using System;
using System.IO;
public class TempFile : IDisposable
{
private readonly string _path;
private bool _disposed = false;
public TempFile(string prefix = "temp_", string suffix = ".tmp")
{
_path = CreateSecureTempFile(prefix, suffix);
}
public string Path => _path;
private string CreateSecureTempFile(string prefix, string suffix)
{
// Use GetRandomFileName for cryptographically random name
string randomName = System.IO.Path.GetFileNameWithoutExtension(System.IO.Path.GetRandomFileName());
string tempFile = System.IO.Path.Combine(
System.IO.Path.GetTempPath(),
$"{prefix}{randomName}{suffix}"
);
// Create file atomically
using (var stream = File.Open(tempFile, FileMode.CreateNew))
{
// File created
}
// Set restrictive permissions
SetOwnerOnlyPermissions(tempFile);
return tempFile;
}
private void SetOwnerOnlyPermissions(string path)
{
var fileInfo = new FileInfo(path);
var security = fileInfo.GetAccessControl();
security.SetAccessRuleProtection(true, false);
var user = System.Security.Principal.WindowsIdentity.GetCurrent();
var rule = new System.Security.AccessControl.FileSystemAccessRule(
user.User,
System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.AccessControlType.Allow
);
security.SetAccessRule(rule);
fileInfo.SetAccessControl(security);
}
public void Write(string content)
{
File.WriteAllText(_path, content);
}
public string Read()
{
return File.ReadAllText(_path);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Dispose managed resources
}
// Delete temp file
try
{
if (File.Exists(_path))
{
File.Delete(_path);
}
}
catch (Exception ex)
{
// Log error but don't throw in Dispose
System.Diagnostics.Debug.WriteLine($"Failed to delete temp file: {ex}");
}
_disposed = true;
}
}
~TempFile()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
// Usage
using (var tempFile = new TempFile())
{
tempFile.Write(sensitiveData);
ProcessFile(tempFile.Path);
}
// Auto-deleted when disposed
Why this works:
- Defense in depth cleanup: Both
Dispose()(explicit cleanup viausingstatements) and finalizer (safety net if disposal forgotten) - Cryptographically random names:
Path.GetRandomFileName()generates 11-character names, much better thanGetTempFileName()'s predictable tmpXXXX.tmp pattern - Atomic file creation:
FileMode.CreateNewfails if file exists, preventing race conditions - Explicit permission control:
SetAccessRuleProtection(true, false)removes inherited permissions and grants access only to current user - Encapsulation: Wrapper class bundles all security logic (random name, atomic creation, secure permissions, guaranteed cleanup) in one reusable component
- Prevents security gaps: Developers can't accidentally skip security steps
- Double-disposal protection:
_disposedflag prevents cleanup errors
When to use: C# applications handling sensitive data that need reusable, foolproof temp file management with automatic cleanup guarantees.
ASP.NET Core file upload with secure temp storage
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using System;
using System.IO;
using System.Threading.Tasks;
[ApiController]
[Route("api/[controller]")]
public class UploadController : ControllerBase
{
[HttpPost]
public async Task<IActionResult> UploadFile(IFormFile file)
{
if (file == null || file.Length == 0)
{
return BadRequest("No file uploaded");
}
// Create secure temp file with random name
string tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
try
{
// Save uploaded file to temp location (CreateNew = atomic creation)
using (var stream = new FileStream(tempFile, FileMode.CreateNew))
{
await file.CopyToAsync(stream);
}
// Set restrictive permissions
SetOwnerOnlyPermissions(tempFile);
// Validate the file
if (!await IsValidFile(tempFile))
{
return BadRequest("Invalid file");
}
// Process the file
var result = await ProcessUploadedFile(tempFile);
return Ok(new { success = true, result });
}
finally
{
// Clean up temp file
if (System.IO.File.Exists(tempFile))
{
System.IO.File.Delete(tempFile);
}
}
}
private void SetOwnerOnlyPermissions(string path)
{
var fileInfo = new FileInfo(path);
var fileSecurity = fileInfo.GetAccessControl();
fileSecurity.SetAccessRuleProtection(true, false);
var currentUser = System.Security.Principal.WindowsIdentity.GetCurrent();
var rule = new System.Security.AccessControl.FileSystemAccessRule(
currentUser.User,
System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.AccessControlType.Allow
);
fileSecurity.SetAccessRule(rule);
fileInfo.SetAccessControl(fileSecurity);
}
private Task<bool> IsValidFile(string path)
{
// Implement file validation (virus scan, content type check, etc.)
return Task.FromResult(true);
}
private Task<string> ProcessUploadedFile(string path)
{
// Process the file
return Task.FromResult("processed");
}
}
Why this works:
- Efficient streaming:
IFormFile.CopyToAsync()streams uploaded file data without loading entire file into memory - Unpredictable temp files:
Path.GetRandomFileName()withFileMode.CreateNewensures atomically-created temp files with random names - Immediate permission lockdown:
SetOwnerOnlyPermissions()called immediately after creation - only current user can access potentially sensitive uploaded data - Early validation: Validates files before processing (file type, size, virus scanning), rejecting dangerous uploads immediately
- Guaranteed cleanup: Try-finally ensures temp file deletion even if validation fails or exceptions occur
- Framework integration: Works seamlessly with
IFormFileinterface, Kestrel, and IIS - Cross-platform: Maintains security across Windows and Linux deployments
When to use: ASP.NET Core applications handling file uploads - especially for user-submitted content that may contain sensitive data or malicious payloads. Always validate uploads before processing to prevent malware, XXE, or injection attacks.
Temporary directory management
using System;
using System.IO;
using System.Security.Cryptography;
public class TempDirectoryManager : IDisposable
{
private readonly string _tempDir;
private bool _disposed = false;
public TempDirectoryManager(string prefix = "tempdir_")
{
string baseTempDir = Path.GetTempPath();
string randomName = GenerateRandomName();
_tempDir = Path.Combine(baseTempDir, $"{prefix}{randomName}");
// Create directory
Directory.CreateDirectory(_tempDir);
// Set restrictive permissions
SetOwnerOnlyPermissions(_tempDir);
}
public string Path => _tempDir;
public string CreateFile(string name, string content)
{
string filePath = System.IO.Path.Combine(_tempDir, name);
File.WriteAllText(filePath, content);
// Ensure file has restrictive permissions
SetOwnerOnlyPermissions(filePath);
return filePath;
}
private string GenerateRandomName()
{
byte[] randomBytes = new byte[16];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomBytes);
}
return BitConverter.ToString(randomBytes).Replace("-", "").ToLower();
}
private void SetOwnerOnlyPermissions(string path)
{
if (Directory.Exists(path))
{
var dirInfo = new DirectoryInfo(path);
var dirSecurity = dirInfo.GetAccessControl();
dirSecurity.SetAccessRuleProtection(true, false);
var user = System.Security.Principal.WindowsIdentity.GetCurrent();
var rule = new System.Security.AccessControl.FileSystemAccessRule(
user.User,
System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.InheritanceFlags.ContainerInherit |
System.Security.AccessControl.InheritanceFlags.ObjectInherit,
System.Security.AccessControl.PropagationFlags.None,
System.Security.AccessControl.AccessControlType.Allow
);
dirSecurity.SetAccessRule(rule);
dirInfo.SetAccessControl(dirSecurity);
}
else if (File.Exists(path))
{
var fileInfo = new FileInfo(path);
var fileSecurity = fileInfo.GetAccessControl();
fileSecurity.SetAccessRuleProtection(true, false);
var user = System.Security.Principal.WindowsIdentity.GetCurrent();
var rule = new System.Security.AccessControl.FileSystemAccessRule(
user.User,
System.Security.AccessControl.FileSystemRights.FullControl,
System.Security.AccessControl.AccessControlType.Allow
);
fileSecurity.SetAccessRule(rule);
fileInfo.SetAccessControl(fileSecurity);
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Cleanup managed resources
}
// Delete directory and all contents
try
{
if (Directory.Exists(_tempDir))
{
Directory.Delete(_tempDir, recursive: true);
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Failed to delete temp directory: {ex}");
}
_disposed = true;
}
}
~TempDirectoryManager()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
// Usage
using (var tempDir = new TempDirectoryManager())
{
string file1 = tempDir.CreateFile("data1.txt", "content1");
string file2 = tempDir.CreateFile("data2.txt", "content2");
BatchProcess(file1, file2);
}
// Directory and all files auto-deleted
Why this works:
- Complete lifecycle management:
TempDirectoryManagerclass handles entire temp directory lifecycle with automatic cleanup viaIDisposable - Cryptographically random directory names:
RandomNumberGeneratorprovides 2^128 possible names, making directories unpredictable - No permission window: Directory created and owner-only ACLs set immediately - no time gap where other users could access it
- Recursive permission handling:
SetOwnerOnlyPermissions()applies to both directories and files viaInheritanceFlags - Inherited protection: Files created within secure directory inherit protection from directory-level permissions
- Proper deletion order:
Dispose()walks directory tree in reverse (files before directories) ensuring successful cleanup - Atomic cleanup: All temp files cleaned up together when directory is disposed
When to use: Batch processing scenarios requiring multiple related temp files - all are isolated in one secure directory and cleaned up atomically. Ideal for complex operations that generate multiple intermediate files.