CWE-377: Insecure Temporary File - JavaScript/Node.js
Overview
Insecure temporary file creation in Node.js occurs when applications create files with predictable names, insecure permissions, or without proper cleanup. Node.js provides several secure libraries like fs.mkdtemp, tmp, and temp that should be used instead of manual file creation in shared directories.
Primary Defence: Use tmp or temp npm packages, or fs.mkdtemp() for secure temporary directory creation with unpredictable names and automatic cleanup.
Common Vulnerable Patterns
Predictable filename in /tmp
const fs = require('fs');
const path = require('path');
// VULNERABLE - Predictable filename using PID
function saveUserData(userData) {
const tempFile = `/tmp/userdata_${process.pid}.txt`;
// Attackers can predict the PID
fs.writeFileSync(tempFile, userData);
processFile(tempFile);
// File not deleted - persists in /tmp
}
Fixed filename in shared directory
const fs = require('fs');
// VULNERABLE - Fixed filename, race condition
function exportCredentials(apiKey, secret) {
const tempFile = '/tmp/credentials.txt';
// Multiple processes might use same filename
// Default permissions may be insecure (0666 - umask)
const data = `API_KEY=${apiKey}\nSECRET=${secret}\n`;
fs.writeFileSync(tempFile, data);
// File not cleaned up
}
Using timestamp for filename
// VULNERABLE - Timestamp-based filename is predictable
function createTempLog() {
const timestamp = Date.now();
const tempFile = `/tmp/log_${timestamp}.txt`;
// Attacker can predict the timestamp
fs.writeFileSync(tempFile, 'Sensitive log data');
return tempFile;
}
Insecure permissions
const fs = require('fs');
// VULNERABLE - World-readable permissions
function saveSensitiveData(data) {
const tempFile = '/tmp/sensitive.txt';
// writeFileSync uses default mode 0666 (modified by umask)
// Often results in 0644 (world-readable)
fs.writeFileSync(tempFile, data);
// Any user can read this file
}
Not cleaning up temporary files
const fs = require('fs');
// VULNERABLE - Temp files accumulate
function processSensitiveData(data) {
const random = Math.floor(Math.random() * 10000);
const tempFile = `/tmp/data_${random}.tmp`;
fs.writeFileSync(tempFile, data);
const result = analyze(tempFile);
// File never deleted - sensitive data persists
return result;
}
Using os.tmpdir() without secure filename
const os = require('os');
const fs = require('fs');
const path = require('path');
// VULNERABLE - Predictable filename even with tmpdir()
function createInsecureTemp(data) {
const tmpDir = os.tmpdir();
const tempFile = path.join(tmpDir, `data_${Date.now()}.txt`);
// Predictable filename
fs.writeFileSync(tempFile, data);
return tempFile;
}
Secure Patterns
Using fs.mkdtemp for secure temporary directory
const fs = require('fs').promises;
const path = require('path');
const os = require('os');
async function processWithSecureTemp(data) {
// Create secure temporary directory with unpredictable name
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'secure-'));
try {
const tempFile = path.join(tmpDir, 'data.txt');
// Write data to file in secure directory
await fs.writeFile(tempFile, data, { mode: 0o600 }); // Owner read/write only
// Process the file
const result = await processFile(tempFile);
return result;
} finally {
// Clean up: delete file and directory
try {
await fs.rm(tmpDir, { recursive: true, force: true });
} catch (err) {
console.error('Failed to clean up temp directory:', err);
}
}
}
Why this works: fs.mkdtemp() creates a directory with a cryptographically random suffix appended to the provided prefix, making the full path unpredictable. On Unix systems, it automatically sets directory permissions to 0700 (owner-only access), preventing other users from listing or accessing contents. Files created within this directory are isolated from other users even if they have default permissions, since directory traversal requires directory permissions. The try-finally block ensures cleanup happens even if processing fails, using fs.rm() with recursive: true to delete the directory and all contents atomically. Setting file mode to 0o600 when creating files provides defense in depth. This pattern is ideal for Node.js applications because it works with async/await, provides strong isolation, and guarantees cleanup.
Using tmp package with auto-cleanup
const tmp = require('tmp');
const fs = require('fs').promises;
// Configure tmp to auto-cleanup on process exit
tmp.setGracefulCleanup();
async function processSecureData(data) {
// Create temp file with secure options
const tmpFile = tmp.fileSync({
mode: 0o600, // Owner read/write only
prefix: 'secure-',
postfix: '.txt',
discardDescriptor: true
});
try {
// Write data to secure temp file
await fs.writeFile(tmpFile.name, data);
// Process the file
const result = await processFile(tmpFile.name);
return result;
} finally {
// Explicit cleanup (tmp also auto-cleans on exit)
tmpFile.removeCallback();
}
}
Why this works: The tmp package generates cryptographically random filenames and creates files with restrictive permissions (mode: 0o600 = owner read/write only). The discardDescriptor: true option closes the file descriptor after creation, preventing file descriptor leaks while still allowing you to access the file by path. setGracefulCleanup() registers process exit handlers to delete temporary files even if the application crashes or is terminated, providing defense in depth beyond explicit cleanup. The removeCallback() in the finally block ensures immediate deletion after use, preventing sensitive data from persisting. This dual cleanup strategy (explicit + exit handler) provides robust protection: explicit cleanup handles normal flow, while graceful cleanup catches abnormal termination scenarios.
Using tmp package with async/await
const tmp = require('tmp-promise');
const fs = require('fs').promises;
async function processWithTmpPromise(data) {
// Create temp file that auto-cleans up
const { path: tempPath, cleanup } = await tmp.file({
mode: 0o600,
prefix: 'secure-',
postfix: '.txt'
});
try {
// Write sensitive data
await fs.writeFile(tempPath, data);
// Process the file
const result = await processFile(tempPath);
return result;
} finally {
// Clean up temp file
await cleanup();
}
}
Why this works: tmp-promise is a promise-based wrapper around the tmp package, providing the same security benefits (cryptographic random names, 0600 permissions) with modern async/await syntax. The cleanup() function returned by tmp.file() is a promise that deletes the file and handles errors gracefully. Using await cleanup() in a finally block ensures cleanup happens even if the processing throws an exception. The promise-based approach integrates seamlessly with async code and prevents callback hell while maintaining security. The temporary file is created atomically with secure permissions before any data is written, preventing race conditions where an attacker might access the file between creation and permission setting.
Using temp package
const temp = require('temp');
const fs = require('fs').promises;
// Track files for automatic cleanup
temp.track();
async function processWithTemp(data) {
// Create temp file with secure permissions
const info = temp.openSync({
prefix: 'secure-',
suffix: '.txt'
});
try {
// Set secure permissions explicitly
await fs.chmod(info.path, 0o600);
// Write data
await fs.writeFile(info.path, data);
// Process the file
const result = await processFile(info.path);
return result;
} finally {
// Clean up (or rely on temp.track() for automatic cleanup)
temp.cleanupSync();
}
}
Why this works: The temp package provides convenience wrappers around Node.js file operations with automatic tracking and cleanup. Calling temp.track() registers exit handlers that automatically delete all created temp files when the process exits, even if explicit cleanup is forgotten. The temp.openSync() creates files with cryptographically random names, though permissions must be set explicitly via fs.chmod(0o600) for maximum security. The cleanupSync() call provides immediate cleanup, while track() serves as a backup for abnormal termination. This dual cleanup strategy provides defense in depth. The synchronous cleanupSync() in a finally block is safe because it's only called after async processing completes. This pattern works well for applications that want automatic cleanup but need explicit control over file creation timing.
Custom secure temp file creation
const fs = require('fs').promises;
const crypto = require('crypto');
const path = require('path');
const os = require('os');
async function createSecureTempFile(prefix = 'secure-', suffix = '.tmp') {
const tmpDir = os.tmpdir();
// Generate cryptographically random filename
const randomName = crypto.randomBytes(16).toString('hex');
const tempPath = path.join(tmpDir, `${prefix}${randomName}${suffix}`);
// Create file with secure permissions (owner only)
await fs.writeFile(tempPath, '', {
mode: 0o600,
flag: 'wx' // Fail if file exists (prevents race conditions)
});
return tempPath;
}
// Usage
async function processData(data) {
const tempPath = await createSecureTempFile();
try {
// Write sensitive data
await fs.writeFile(tempPath, data);
// Process the file
const result = await processFile(tempPath);
return result;
} finally {
// Always clean up
try {
await fs.unlink(tempPath);
} catch (err) {
if (err.code !== 'ENOENT') {
console.error('Failed to delete temp file:', err);
}
}
}
}
Why this works: Using crypto.randomBytes(16) generates 16 bytes (128 bits) of cryptographically strong random data from the OS's secure random number generator (/dev/urandom on Unix, CryptGenRandom on Windows), providing 2^128 possible filenames. The hexadecimal encoding produces a 32-character string that's both unpredictable and filesystem-safe. The flag: 'wx' option in fs.writeFile() combines write and exclusive flags, making file creation atomic - it fails if the file exists, preventing race conditions. Setting mode: 0o600 ensures owner-only permissions from the moment of creation. This manual approach provides complete control over the temp file lifecycle while maintaining the security properties of library solutions. It's ideal when you need specific filename patterns or when existing libraries don't meet your requirements.
Temporary file manager class
const fs = require('fs').promises;
const crypto = require('crypto');
const path = require('path');
const os = require('os');
class TempFileManager {
constructor() {
this.files = new Set();
this.cleanupRegistered = false;
// Register cleanup on process exit
if (!this.cleanupRegistered) {
process.on('exit', () => this.cleanupSync());
process.on('SIGINT', () => {
this.cleanupSync();
process.exit(130);
});
this.cleanupRegistered = true;
}
}
async createTempFile(options = {}) {
const {
prefix = 'temp-',
suffix = '.tmp',
mode = 0o600
} = options;
const tmpDir = os.tmpdir();
const randomName = crypto.randomBytes(16).toString('hex');
const filePath = path.join(tmpDir, `${prefix}${randomName}${suffix}`);
// Create with secure permissions
await fs.writeFile(filePath, '', { mode, flag: 'wx' });
// Track for cleanup
this.files.add(filePath);
return filePath;
}
async createTempDir(options = {}) {
const { prefix = 'tempdir-' } = options;
const tmpDir = os.tmpdir();
// Use mkdtemp for secure directory creation
const dirPath = await fs.mkdtemp(path.join(tmpDir, prefix));
// Ensure secure permissions
await fs.chmod(dirPath, 0o700);
this.files.add(dirPath);
return dirPath;
}
async cleanup() {
for (const file of this.files) {
try {
const stat = await fs.stat(file);
if (stat.isDirectory()) {
await fs.rm(file, { recursive: true, force: true });
} else {
await fs.unlink(file);
}
} catch (err) {
if (err.code !== 'ENOENT') {
console.error(`Failed to clean up ${file}:`, err);
}
}
}
this.files.clear();
}
cleanupSync() {
const fsSync = require('fs');
for (const file of this.files) {
try {
const stat = fsSync.statSync(file);
if (stat.isDirectory()) {
fsSync.rmSync(file, { recursive: true, force: true });
} else {
fsSync.unlinkSync(file);
}
} catch (err) {
if (err.code !== 'ENOENT') {
console.error(`Failed to clean up ${file}:`, err);
}
}
}
this.files.clear();
}
}
// Usage
async function processMultipleFiles(dataArray) {
const manager = new TempFileManager();
try {
const files = await Promise.all(
dataArray.map(async (data) => {
const file = await manager.createTempFile({ prefix: 'data-' });
await fs.writeFile(file, data);
return file;
})
);
return await batchProcess(files);
} finally {
await manager.cleanup();
}
}
Why this works: The TempFileManager class centralizes temp file lifecycle management, ensuring consistent security practices. Registering cleanup handlers for exit, SIGINT signals ensures temp files are deleted even if the process crashes or is killed. Using crypto.randomBytes(16) for filenames provides cryptographic randomness (2^128 possibilities). The flag: 'wx' in fs.writeFile() creates files exclusively (fails if exists), preventing race conditions. Tracking files in a Set enables bulk cleanup of all temp files at once. The dual cleanup strategy (async cleanup() and sync cleanupSync()) handles both normal async operation and synchronous exit handlers. Checking for ENOENT errors prevents exceptions when files are already deleted. This pattern is ideal for complex Node.js applications that create many temp files and need guaranteed cleanup.
Express file upload with secure temp storage
const express = require('express');
const multer = require('multer');
const tmp = require('tmp-promise');
const fs = require('fs').promises;
const app = express();
// Configure multer to use secure temp storage
const storage = multer.diskStorage({
destination: async (req, file, cb) => {
try {
// Create secure temp directory for each upload
const { path: tmpDir } = await tmp.dir({
mode: 0o700,
prefix: 'upload-',
unsafeCleanup: false
});
cb(null, tmpDir);
} catch (err) {
cb(err);
}
},
filename: (req, file, cb) => {
// Generate secure random filename
const crypto = require('crypto');
const randomName = crypto.randomBytes(16).toString('hex');
const ext = require('path').extname(file.originalname);
cb(null, `${randomName}${ext}`);
}
});
const upload = multer({
storage,
limits: { fileSize: 10 * 1024 * 1024 } // 10MB limit
});
app.post('/upload', upload.single('file'), async (req, res) => {
const uploadedFile = req.file;
try {
// Set secure permissions
await fs.chmod(uploadedFile.path, 0o600);
// Validate and process the file
if (!await isValidFile(uploadedFile.path)) {
return res.status(400).send('Invalid file');
}
const result = await processUploadedFile(uploadedFile.path);
res.json({ success: true, result });
} finally {
// Clean up temp file and directory
try {
await fs.unlink(uploadedFile.path);
await fs.rmdir(require('path').dirname(uploadedFile.path));
} catch (err) {
console.error('Cleanup failed:', err);
}
}
});
Why this works: Multer's custom storage configuration allows complete control over where and how uploaded files are stored. Using tmp.dir() for the destination creates a new secure directory (mode: 0o700) for each upload, providing strong isolation. Generating filenames with crypto.randomBytes(16) ensures unpredictability while preserving the file extension for type validation. Setting mode: 0o600 via fs.chmod() after saving ensures only the owner can access the uploaded file. The pattern validates files (content type, virus scan, size checks) before processing, rejecting malicious uploads. Cleanup happens in a finally block, deleting both the file and its dedicated directory. This approach is ideal for Express applications because it integrates with Multer middleware while maintaining security. The per-upload directory isolation prevents attackers from accessing other users' uploads even if they guess a filename.
Next.js API route with secure temp files
import tmp from 'tmp-promise';
import fs from 'fs/promises';
import formidable from 'formidable';
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// Create secure temp directory
const { path: tmpDir, cleanup } = await tmp.dir({
mode: 0o700,
prefix: 'nextjs-upload-'
});
try {
// Parse form with secure temp storage
const form = formidable({
uploadDir: tmpDir,
keepExtensions: true,
maxFileSize: 10 * 1024 * 1024, // 10MB
});
const [fields, files] = await new Promise((resolve, reject) => {
form.parse(req, (err, fields, files) => {
if (err) reject(err);
else resolve([fields, files]);
});
});
const uploadedFile = files.file[0];
// Set secure permissions
await fs.chmod(uploadedFile.filepath, 0o600);
// Process the file
const result = await processFile(uploadedFile.filepath);
res.status(200).json({ success: true, result });
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: 'Upload failed' });
} finally {
// Clean up temp directory and all files
await cleanup();
}
}
Why this works: Next.js API routes handle server-side requests, making them suitable for secure file uploads. Creating a secure temp directory with tmp.dir() (mode: 0o700) isolates each upload request's files. Using formidable with a custom uploadDir ensures uploaded files go directly to the secure directory. The maxFileSize limit prevents denial-of-service attacks from huge uploads. Setting keepExtensions: true preserves file type information for validation. Setting mode: 0o600 on uploaded files via fs.chmod() ensures owner-only access. The cleanup() function from tmp-promise recursively deletes the entire temp directory and all uploaded files, even on errors. This pattern is ideal for Next.js applications because it works in the API route context, handles form parsing securely, and integrates with React frontends. The automatic cleanup ensures no temp files persist between requests.