Skip to content

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.

Additional Resources