Skip to content

CWE-377: Insecure Temporary File - Go

Overview

Insecure temporary file vulnerabilities in Go applications occur when temporary files are created with predictable names or insecure permissions, enabling attacks like race conditions, information disclosure, or denial of service. Temporary files are commonly used for caching, processing uploaded data, storing intermediate computation results, or facilitating inter-process communication. When created insecurely, they become attack vectors.

The primary risks include predictable filenames allowing attackers to create files before the application (winning the race condition), overly permissive file permissions exposing sensitive data to other users on shared systems, insecure temporary directories allowing symlink attacks where attackers replace temp files with symbolic links to critical system files, and temp file leaks where files aren't deleted after use, accumulating sensitive data. On Unix systems, the default /tmp directory is world-writable with the sticky bit, allowing any user to create files but preventing deletion of others' files - however, files can still be read if permissions allow.

Go's os.CreateTemp function (and the older ioutil.TempFile) provides secure defaults: random unpredictable filenames using crypto/rand, restrictive permissions (0600 on Unix: owner read/write only), and atomic file creation preventing race conditions. However, developers sometimes bypass these safe defaults by manually constructing temp file paths, using world-readable permissions, or failing to clean up temp files properly.

Primary Defence: Use os.CreateTemp for all temporary file creation - it provides cryptographically random filenames with secure permissions. Always defer file cleanup with os.Remove. Never construct temp file paths manually. For sensitive data, consider encrypting temp file contents. Ensure temp files are created in appropriate directories with proper access controls.

Common Vulnerable Patterns

Predictable Temp File Names

// VULNERABLE - Predictable temporary filenames
package main

import (
    "fmt"
    "os"
    "time"
)

func processUpload(userID int, data []byte) error {
    // DANGEROUS: Predictable filename based on timestamp
    timestamp := time.Now().Unix()
    filename := fmt.Sprintf("/tmp/upload_%d_%d.tmp", userID, timestamp)

    // Create file
    file, err := os.Create(filename)
    if err != nil {
        return err
    }
    defer file.Close()

    // Write sensitive data
    _, err = file.Write(data)
    return err
}

// ATTACK (Race Condition):
// 1. Attacker predicts filename: /tmp/upload_1234_1707235200.tmp
// 2. Creates file with that name before application (with symlink to /etc/passwd)
// 3. Application writes to what it thinks is temp file, overwrites system file
//
// ATTACK (Information Disclosure):
// Attacker knows file pattern, reads files: /tmp/upload_*

Why this is vulnerable: Timestamps are predictable - attackers can guess filenames within the second or microsecond range. Sequential user IDs make prediction even easier. Attackers can pre-create files with known names, winning the race condition (TOCTOU: Time of Check to Time of Use). If the pre-created file is a symbolic link to a critical system file, the application overwrites that file with user data, potentially gaining code execution or causing denial of service. Additionally, predictable names allow attackers to enumerate and read temp files, exposing sensitive data.

Insecure File Permissions

// VULNERABLE - World-readable temporary files
import (
    "os"
)

func createTempConfig(configData []byte) (string, error) {
    // Create temp file with default permissions
    file, err := os.CreateTemp("", "config-*.json")
    if err != nil {
        return "", err
    }

    // DANGEROUS: Making file world-readable
    os.Chmod(file.Name(), 0644) // rw-r--r--

    // Write sensitive configuration (API keys, database credentials)
    file.Write(configData)
    file.Close()

    return file.Name(), nil
}

// VULNERABILITY:
// File is readable by all users on the system
// On shared hosting, VPS, containers, other users can read temp files
// Sensitive data (credentials, tokens) exposed

Why this is vulnerable: os.CreateTemp creates files with 0600 permissions (owner read/write only) by default. Explicitly changing permissions to 0644 makes files world-readable, exposing their contents to all users on the system. On multi-tenant systems (shared hosting, containers in Kubernetes, VPS), any process or user can read these files. If temp files contain sensitive data (credentials, session tokens, PII), this is a direct information disclosure vulnerability. Even on single-user systems, malware or compromised processes can access the data.

Hard-Coded Temp Directory

// VULNERABLE - Hard-coded /tmp directory
import (
    "os"
    "path/filepath"
)

func saveProcessingResult(data []byte) error {
    // DANGEROUS: Hard-coded /tmp on Windows might not exist
    // Also ignores user preferences and security policies
    tempDir := "/tmp"

    // Still creates predictable path
    tempFile := filepath.Join(tempDir, "result.tmp")

    return os.WriteFile(tempFile, data, 0600)
}

// ISSUES:
// 1. /tmp may not exist on Windows
// 2. Ignores environment variables (TMPDIR, TEMP, TMP)
// 3. May violate security policies requiring encrypted temp storage
// 4. Fixed filename allows attackers to pre-create symlinks

Why this is vulnerable: Hard-coding /tmp breaks on Windows (which uses C:\Temp or user-specific temp directories). It ignores environment variables like TMPDIR (Unix) or TEMP/TMP (Windows) that allow users and admins to configure temp storage locations. Security policies might require temp files on encrypted volumes, which hard-coding bypasses. The fixed filename "result.tmp" allows race condition attacks - attackers pre-create it as a symlink to a target file. Using os.TempDir() respects system configuration and security policies.

Temp Files Not Deleted

// VULNERABLE - Temp file leakage
import (
    "encoding/json"
    "os"
)

func processData(input map[string]interface{}) error {
    // Create temp file
    tempFile, err := os.CreateTemp("", "data-*.json")
    if err != nil {
        return err
    }
    // Missing: defer tempFile.Close() and defer os.Remove(tempFile.Name())

    // Write data
    encoder := json.NewEncoder(tempFile)
    if err := encoder.Encode(input); err != nil {
        // DANGEROUS: Return without cleanup on error
        return err
    }

    // Process...
    readAndProcess(tempFile.Name())

    // DANGEROUS: Only cleaned up on success path
    tempFile.Close()
    os.Remove(tempFile.Name())

    return nil
}

// VULNERABILITY:
// Temp files accumulate on error paths
// Sensitive data persists on disk indefinitely
// Eventually fills /tmp, causing DoS

Why this is vulnerable: Without defer, temp files aren't deleted if errors occur before the cleanup code. Over time, abandoned temp files accumulate in /tmp, consuming disk space and potentially causing denial of service when the partition fills. More critically, sensitive data persists indefinitely - files containing credentials, PII, or business-critical data remain readable. Cleanup must be guaranteed using defer immediately after file creation, ensuring deletion even if panics or errors occur.

// VULNERABLE - Following symlinks in temp directory
import (
    "fmt"
    "os"
    "path/filepath"
)

func createUserCache(userID int) error {
    cacheDir := filepath.Join("/tmp", fmt.Sprintf("user_%d", userID))

    // DANGEROUS: Create directory without checking if it's a symlink
    if err := os.MkdirAll(cacheDir, 0700); err != nil {
        return err
    }

    // Write cache data
    cachePath := filepath.Join(cacheDir, "cache.dat")
    return os.WriteFile(cachePath, []byte("cache data"), 0600)
}

// ATTACK:
// 1. Attacker creates symlink: /tmp/user_1234 -> /home/victim/.ssh
// 2. Application follows symlink, creates cache.dat in victim's .ssh directory
// 3. Attacker may overwrite authorized_keys or other critical files

Why this is vulnerable: In world-writable directories like /tmp, attackers can create symbolic links with names the application will use. When the application creates files or directories at those paths, the OS follows the symlink, creating files at the attacker-controlled target location. This can overwrite critical system files, configuration files, or enable privilege escalation. Properly using os.CreateTemp with the O_EXCL flag (set internally) prevents this by failing if a file already exists, but manually constructing paths is vulnerable.

Secure Patterns

Using os.CreateTemp Properly

// SECURE - Proper temporary file handling
package main

import (
    "fmt"
    "os"
)

func processDataSecurely(data []byte) error {
    // SECURE: CreateTemp uses crypto/rand for filename, 0600 permissions
    tempFile, err := os.CreateTemp("", "secure-*.tmp")
    if err != nil {
        return fmt.Errorf("failed to create temp file: %w", err)
    }

    // SECURE: Guarantee cleanup even on error/panic
    defer os.Remove(tempFile.Name())
    defer tempFile.Close()

    // Write data
    if _, err := tempFile.Write(data); err != nil {
        return fmt.Errorf("write failed: %w", err)
    }

    // Sync to ensure data is written before processing
    if err := tempFile.Sync(); err != nil {
        return fmt.Errorf("sync failed: %w", err)
    }

    // Process the temp file
    if err := processTempFile(tempFile.Name()); err != nil {
        return err
    }

    return nil
}

func processTempFile(path string) error {
    // Processing logic
    return nil
}

Why this works: os.CreateTemp("", "secure-*.tmp") creates a file in the system's default temp directory (respecting TMPDIR/TEMP environment variables) with a cryptographically random filename matching the pattern "secure-XXXXXXXX.tmp". The file is created with 0600 permissions (owner read/write only), preventing other users from accessing it. The O_EXCL flag ensures atomic creation - if a file exists, creation fails, preventing race conditions. defer statements guarantee cleanup in all code paths (success, error, panic). Sync() ensures data is flushed to disk before processing, preventing corruption if the process crashes.

Secure Temporary Directory Creation

// SECURE - Creating temporary directories safely
import (
    "fmt"
    "os"
    "path/filepath"
)

func createSecureTempDir() (string, func(), error) {
    // SECURE: MkdirTemp uses crypto/rand, 0700 permissions
    tempDir, err := os.MkdirTemp("", "processing-*")
    if err != nil {
        return "", nil, fmt.Errorf("failed to create temp dir: %w", err)
    }

    // Return cleanup function
    cleanup := func() {
        os.RemoveAll(tempDir)
    }

    return tempDir, cleanup, nil
}

// Usage pattern
func processMultipleFiles(files [][]byte) error {
    tempDir, cleanup, err := createSecureTempDir()
    if err != nil {
        return err
    }
    defer cleanup()

    // Work with multiple temp files in secure directory
    for i, data := range files {
        filename := filepath.Join(tempDir, fmt.Sprintf("file_%d.dat", i))
        if err := os.WriteFile(filename, data, 0600); err != nil {
            return err
        }
    }

    // Process files in temp directory
    return processDirectory(tempDir)
}

func processDirectory(path string) error {
    return nil
}

Why this works: os.MkdirTemp creates a directory with a cryptographically random name and 0700 permissions (owner access only). This prevents other users from listing directory contents or accessing files within. Creating files within this secure directory inherits its access restrictions. Returning a cleanup function encapsulates cleanup logic, making it easy to use with defer. os.RemoveAll recursively deletes the directory and all contents, ensuring complete cleanup. This pattern is ideal when working with multiple related temp files that should be grouped.

Specifying Custom Temp Directory

// SECURE - Using specific temp directory with security controls
import (
    "os"
    "path/filepath"
)

func createTempInSecureLocation() (*os.File, error) {
    // SECURE: Use application-specific temp directory
    appTempDir := filepath.Join(os.TempDir(), "myapp-temp")

    // Ensure directory exists with restrictive permissions
    if err := os.MkdirAll(appTempDir, 0700); err != nil {
        return nil, err
    }

    // SECURE: Create temp file in controlled directory
    tempFile, err := os.CreateTemp(appTempDir, "data-*.tmp")
    if err != nil {
        return nil, err
    }

    return tempFile, nil
}

Why this works: Creating an application-specific subdirectory within the system temp directory (accessed via os.TempDir()) provides additional isolation. The subdirectory has 0700 permissions, preventing other users from listing its contents. Files created within inherit protection from the parent directory structure. This allows application-specific cleanup policies (periodic purging of old temp files) and makes temp file locations predictable for debugging. os.TempDir() still respects system configuration while adding application-level organization.

Encrypting Sensitive Temp Files

// SECURE - Encrypting temporary files
import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "io"
    "os"
)

func createEncryptedTempFile(key, plaintext []byte) (string, error) {
    // Create temp file
    tempFile, err := os.CreateTemp("", "encrypted-*.tmp")
    if err != nil {
        return "", err
    }
    tempPath := tempFile.Name()

    // Encrypt data before writing
    ciphertext, err := encryptData(key, plaintext)
    if err != nil {
        tempFile.Close()
        os.Remove(tempPath)
        return "", err
    }

    // Write encrypted data
    if _, err := tempFile.Write(ciphertext); err != nil {
        tempFile.Close()
        os.Remove(tempPath)
        return "", err
    }

    tempFile.Close()
    return tempPath, nil
}

func encryptData(key, plaintext []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    nonce := make([]byte, gcm.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return nil, err
    }

    return gcm.Seal(nonce, nonce, plaintext, nil), nil
}

func readAndDecryptTempFile(path string, key []byte) ([]byte, error) {
    // Read encrypted data
    ciphertext, err := os.ReadFile(path)
    if err != nil {
        return nil, err
    }

    // Decrypt
    return decryptData(key, ciphertext)
}

func decryptData(key, ciphertext []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    nonceSize := gcm.NonceSize()
    if len(ciphertext) < nonceSize {
        return nil, fmt.Errorf("ciphertext too short")
    }

    nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
    return gcm.Open(nil, nonce, ciphertext, nil)
}

Why this works: Even with secure file permissions, encrypting temp file contents provides defense-in-depth. If permissions are misconfigured, backups include temp files, or disk forensics is performed, encrypted data remains protected. AES-256-GCM provides confidentiality and integrity. The encryption key should be stored securely (in memory, encrypted configuration, or key vault), never in the temp file itself. This pattern is essential for highly sensitive data like cryptographic keys, medical records, or financial data. The performance overhead of encryption is usually acceptable for temp file use cases.

Safe Cleanup with Error Handling

// SECURE - Robust cleanup with error handling
import (
    "fmt"
    "log"
    "os"
)

func processWithRobustCleanup(data []byte) error {
    tempFile, err := os.CreateTemp("", "process-*.tmp")
    if err != nil {
        return err
    }
    tempPath := tempFile.Name()

    // SECURE: Cleanup that logs errors but doesn't block
    defer func() {
        if err := tempFile.Close(); err != nil {
            log.Printf("Warning: failed to close temp file %s: %v", tempPath, err)
        }

        if err := os.Remove(tempPath); err != nil {
            log.Printf("Warning: failed to remove temp file %s: %v", tempPath, err)
        }
    }()

    // Write and process
    if _, err := tempFile.Write(data); err != nil {
        return fmt.Errorf("write failed: %w", err)
    }

    if err := processTempFile(tempPath); err != nil {
        return fmt.Errorf("processing failed: %w", err)
    }

    return nil
}

Why this works: Cleanup errors (file already deleted, permission denied) shouldn't cause the main function to fail, but should be logged for debugging. The defer func() closure allows handling cleanup errors independently - logging warnings without returning errors. This prevents cleanup failures from masking the original error. Closing the file before removing it is a best practice, though os.Remove works on open files on Unix (but not Windows). Logging cleanup issues aids troubleshooting without impacting application logic.

Testing with Temp Files

// SECURE - Using temp files in tests
package main_test

import (
    "os"
    "testing"
)

func TestDataProcessing(t *testing.T) {
    // SECURE: Create temp file for test
    tempFile, err := os.CreateTemp("", "test-*.dat")
    if err != nil {
        t.Fatalf("Failed to create temp file: %v", err)
    }

    // SECURE: Clean up after test
    t.Cleanup(func() {
        tempFile.Close()
        os.Remove(tempFile.Name())
    })

    // Write test data
    testData := []byte("test content")
    if _, err := tempFile.Write(testData); err != nil {
        t.Fatalf("Write failed: %v", err)
    }
    tempFile.Sync()

    // Test function that processes the file
    result, err := processFile(tempFile.Name())
    if err != nil {
        t.Errorf("Processing failed: %v", err)
    }

    // Assertions
    if result != "expected" {
        t.Errorf("Got %s, want expected", result)
    }
}

func processFile(path string) (string, error) {
    return "expected", nil
}

Why this works: t.Cleanup() registers cleanup functions that run after the test completes (even if the test fails or panics), similar to defer but test-aware. This ensures temp files created during tests are always cleaned up. Using os.CreateTemp in tests provides the same security benefits as production code. Tests run in parallel can safely create temp files without naming conflicts (due to random filenames). This pattern keeps test environments clean and prevents disk space exhaustion from accumulated test data.

Additional Resources