Stop arbitrary file write attacks by implementing strict validation and secure storage. Learn how to protect your Node.js and PHP apps from file overwrites.
During a recent security audit of a legacy application, I found a classic flaw: the profile picture upload feature allowed users to specify a filename that the server wrote directly to the web root. An attacker could have easily overwritten the index.php or package.json file, leading to full remote code execution. It’s a terrifyingly simple mistake that happens because we often trust the metadata sent by the client.
Preventing an arbitrary file write requires shifting your mindset. You must assume that every piece of data sent from a browser—including the filename—is malicious.
When a user uploads a file, your server receives a multipart/form-data request. If your code takes the filename provided by the Content-Disposition header and uses it to save the file on disk, you’ve opened the door.
We first tried to sanitize the input using simple string replacement, stripping out dots and slashes. It broke because some legitimate filenames contain non-ASCII characters that the regex didn't account for, and it didn't stop attackers from using encoded sequences. We eventually realized that sanitizing the user's input is a losing battle. Instead, we switched to a "generate-on-save" strategy.
To implement secure file uploads, you need to treat the incoming file as a raw stream and ignore the client's naming suggestions entirely. Here is the high-level strategy:
MIME type sent by the browser. Use a library like file-type in Node.js or finfo in PHP to inspect the actual file signature (magic bytes).In Node.js, libraries like multer are standard, but they need to be configured carefully. If you're using Express, avoid the default disk storage configuration if it allows user-controlled paths.
JAVASCRIPTconst multer = require(CE9178">'multer'); const { v4: uuidv4 } = require(CE9178">'uuid'); const path = require(CE9178">'path'); const storage = multer.diskStorage({ destination: CE9178">'/var/www/uploads/', filename: (req, file, cb) => { // Ignore originalname, use a safe, generated ID cb(null, CE9178">`${uuidv4()}${path.extname(file.originalname)}`); } }); const upload = multer({ storage: storage, limits: { fileSize: 2 * 1024 * 1024 } // 2MB limit });
PHP’s move_uploaded_file() is your best friend, provided you don't concatenate the user's filename to your path.
PHP$uploadDir = '/var/www/uploads/'; $safeName = bin2hex(random_bytes(16)); #6A9955">// Generate a random string $extension = pathinfo($_FILES['userfile']['name'], PATHINFO_EXTENSION); $destination = $uploadDir . $safeName . '.' . $extension; if (move_uploaded_file($_FILES['userfile']['tmp_name'], $destination)) { echo "File uploaded successfully."; }
Even with randomized names, you must ensure the application cannot be coerced into writing files outside the designated directory. This is critical for path traversal prevention. If you’re manually constructing paths, you must resolve the absolute path and verify it starts with your expected base directory.
If you are interested in deeper file system security, I've written about Preventing Path Traversal: Secure File System Access for Developers which explains these directory-level checks in detail. Additionally, remember that secure handling is only one layer; you should also look into Secure file uploads from the ground up: A developer's guide to ensure you aren't leaving other doors open.
Q: Can I just whitelist extensions like .jpg and .png?
A: Whitelisting is good, but it's not enough. Attackers can upload a file named shell.php.jpg or embed malicious code within the metadata of an actual image file. Always validate the magic bytes.
Q: Is it safe to store files in the web root if I rename them? A: It's risky. If an attacker finds a way to upload a script, they might find a way to execute it if your server is misconfigured. Storing files outside the web root or using a dedicated CDN/S3 bucket is the industry standard for a reason.
Q: What about file deserialization? A: If you're processing files that contain serialized data, you're at risk of remote code execution even if the upload process is secure. Check out Preventing Improper File Deserialization: A Guide for Node.js and PHP for more on that specific danger.
I’m still not 100% comfortable with local disk storage for production apps. If I were starting a new project today, I would skip local storage entirely and stream uploads directly to an S3-compatible bucket with strict IAM policies. It simplifies the security model significantly. Regardless of your architecture, the core takeaway remains: never trust the client's metadata, and always isolate user-provided files from your source code and system configuration. Preventing an arbitrary file write is about removing the attacker's ability to influence the filesystem state, not just cleaning up their input.
TOCTOU race conditions leave your file system operations vulnerable. Learn how to secure your Node.js and PHP code against these concurrency flaws today.
Read moreLearn to prevent integer overflow and underflow in Node.js and PHP. Discover how to handle large numbers securely and avoid silent data corruption today.