Arbitrary file write vulnerability from unvalidated user-controlled file paths

Critical Risk path-traversal
nodejsfile-writepath-traversalarbitrary-writercefilesystem

What it is

A critical vulnerability where attackers can write files to arbitrary locations on the file system by controlling file paths in Node.js file operations. This can lead to overwriting critical system files, configuration files, or placing malicious files in web-accessible directories, potentially resulting in remote code execution, privilege escalation, or complete system compromise.

const express = require('express');
const fs = require('fs');
const path = require('path');

const app = express();
app.use(express.json());


app.post('/write-file', (req, res) => {
    const { filename, content } = req.body;
    
    // DANGEROUS: No path validation
    const filePath = './data/' + filename;
    
    fs.writeFile(filePath, content, (err) => {
        if (err) {
            res.status(500).send('Error writing file');
        } else {
            res.send('File written successfully');
        }
    });
});

const express = require('express');
const fs = require('fs').promises;
const path = require('path');

const app = express();
app.use(express.json());

function validatePath(userInput, baseDir) {
    if (!userInput || typeof userInput !== 'string') {
        throw new Error('Invalid filename');
    }
    
    // Remove null bytes and check for traversal
    const cleaned = userInput.replace(/\x00/g, '').trim();
    
    if (cleaned.includes('..') || cleaned.includes('\\')) {
        throw new Error('Path traversal detected');
    }
    
    // Extract basename only (removes path components)
    const basename = path.basename(cleaned);
    
    // Resolve full path
    const fullPath = path.resolve(baseDir, basename);
    
    // Ensure path is within base directory
    const relativePath = path.relative(baseDir, fullPath);
    
    if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
        throw new Error('Path outside base directory');
    }
    
    return fullPath;
}

const DATA_DIR = path.resolve('./data');

app.post('/write-file', async (req, res) => {
    try {
        const { filename, content } = req.body;
        
        if (!filename || !content) {
            return res.status(400).json({ error: 'Missing parameters' });
        }
        
        // Validate and resolve path
        const safePath = validatePath(filename, DATA_DIR);
        
        // Write file with restricted permissions
        await fs.writeFile(safePath, content, { mode: 0o644 });
        
        res.json({ 
            message: 'File written successfully',
            filename: path.basename(safePath)
        });
        
    } catch (error) {
        console.error('File write error:', error.message);
        res.status(400).json({ error: error.message });
    }
});

app.listen(3000);

💡 Why This Fix Works

The vulnerable code allows users to control file paths directly through string concatenation, enabling path traversal attacks to write files anywhere on the filesystem. The secure version implements comprehensive path validation using path.resolve() and path.relative() to ensure all file operations stay within the intended base directory. It uses path.basename() to extract only the filename (removing path components), validates that resolved paths don't escape the base directory, and removes dangerous characters like null bytes. This prevents attackers from writing files outside the designated directory.

Why it happens

User input directly controls file paths in fs operations

Root causes

User controlled paths

User input directly controls file paths in fs operations

Paths not validated

Missing path validation before file write operations

Weak sanitization

Weak input sanitazation allows path traversal sequences

Direct string concatenation

User inputs are directly concatenated with base paths and could allow the user to traverse directories outside of the intended paths.

No verification for resolved paths

Without verification for valid resolved paths, you could traverse to other directories.

Fixes

1

Use Path Validation Functions

Use path.resolve() and path.relative() to validate and normalize file paths before any file operations. These Node.js built-in functions help ensure paths are resolved correctly and can be checked against expected base directories.

2

Enforce Base Directory Containment

After resolving paths, always verify that the final resolved path stays within the intended base directory. Check that the relative path doesn't start with '..' and isn't an absolute path to prevent directory traversal attacks.

3

Extract Only Filename Components

Use path.basename() to extract only the filename component from user input, removing any directory path components. This prevents users from including path traversal sequences like '../' in their input.

4

Implement Filename Whitelist Validation

Create and enforce a whitelist of allowed filenames or filename patterns. Validate that uploaded filenames match expected patterns and reject any that don't conform to your security policy.

5

Avoid Direct String Concatenation

Never directly concatenate user input with file paths using string operations. Always use proper path manipulation functions like path.join() or path.resolve() which handle path separators and edge cases correctly across different operating systems.

Detect This Vulnerability in Your Code

Sourcery automatically identifies arbitrary file write vulnerability from unvalidated user-controlled file paths and many other security issues in your codebase.