
Secure File Handling in Vibe Coding: Best Practices and Implementation
Introduction
File upload functionality is a common requirement in modern web applications, enabling users to share documents, images, videos, and other content. However, this seemingly simple feature can introduce significant security vulnerabilities if not implemented correctly. In the rapidly evolving landscape of vibe coding, where AI tools generate code based on natural language prompts, file handling and upload security often receive inadequate attention, creating dangerous attack vectors that malicious actors can exploit.
According to a recent study by the Web Application Security Consortium, file upload vulnerabilities were present in 68% of applications built with AI coding assistants, compared to 42% in traditionally coded applications. This alarming statistic highlights the critical importance of understanding and addressing file handling security risks in vibe-coded applications.
When developers instruct AI to “add file upload functionality” or “create an image upload feature,” the resulting code often prioritizes basic functionality over security, generating implementations that lack proper validation, sanitization, storage security, and access controls. These oversights can lead to severe security breaches, including arbitrary code execution, server-side request forgery, cross-site scripting, and unauthorized access to sensitive data.
This article provides a comprehensive guide to implementing secure file handling in vibe-coded applications built with popular full-stack app builders like Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit. We’ll examine common file upload vulnerabilities in AI-generated code, demonstrate secure implementation approaches for each platform, and provide practical techniques for enhancing file handling security. By following these practices, you can ensure that your vibe-coded applications maintain robust protection against file-related threats while still benefiting from the rapid development that AI tools enable.
Common File Upload Vulnerabilities in AI-Generated Code
AI coding assistants typically follow certain patterns when generating file upload functionality. Understanding these patterns and their associated vulnerabilities is the first step toward implementing secure file handling.
Insufficient File Type Validation
One of the most common vulnerabilities in AI-generated file upload code is inadequate file type validation:
// Example of insecure file type validation in AI-generated code
app.post('/upload', upload.single('file'), (req, res) => {
const file = req.file;
// Vulnerable: Relying only on Content-Type header
if (file.mimetype.startsWith('image/')) {
// Process image file
// ...
res.json({ message: 'Image uploaded successfully' });
} else {
res.status(400).json({ error: 'Only image files are allowed' });
}
});
This approach is vulnerable because:
- It relies solely on the Content-Type header, which can be easily spoofed
- It doesn’t verify the actual file content
- It allows attackers to upload malicious files disguised as images
- It creates a false sense of security
Insecure File Name Handling
AI-generated code often uses unsanitized user-provided filenames:
// Example of insecure filename handling in AI-generated code
app.post('/upload', upload.single('file'), (req, res) => {
const file = req.file;
// Vulnerable: Using original filename without sanitization
const fileName = file.originalname;
const filePath = path.join(uploadDir, fileName);
fs.renameSync(file.path, filePath);
res.json({
message: 'File uploaded successfully',
heroImage: "/images/blog/secure-file-handling-and-upload-security-in-vibe-coded-applications.jpg"
});
});
This code is vulnerable because:
- It uses unsanitized user-provided filenames
- It allows path traversal attacks (e.g.,
../../../etc/passwd
) - It enables overwriting of existing files
- It can lead to inconsistent behavior with special characters
Unrestricted File Size
AI tools often generate code without file size restrictions:
// Example of missing file size restrictions in AI-generated code
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
// No file size validation
// Process uploaded file
// ...
res.json({ message: 'File uploaded successfully' });
});
This approach is problematic because:
- It allows uploading of arbitrarily large files
- It enables denial of service attacks by exhausting disk space
- It can cause server performance issues
- It may lead to unexpected application behavior
Insecure File Storage
AI-generated code frequently uses insecure file storage practices:
// Example of insecure file storage in AI-generated code
app.post('/upload', upload.single('file'), (req, res) => {
const file = req.file;
// Vulnerable: Storing in public directory with predictable naming
const fileName = Date.now() + '-' + file.originalname;
const filePath = path.join('public/uploads', fileName);
fs.renameSync(file.path, filePath);
// Vulnerable: Returning direct file path
res.json({
message: 'File uploaded successfully',
heroImage: "/images/blog/secure-file-handling-and-upload-security-in-vibe-coded-applications.jpg"
});
});
This code is vulnerable because:
- Files are stored in a publicly accessible directory
- File naming is predictable and can be guessed
- Direct file paths are exposed to users
- No access controls are implemented for uploaded files
Missing Content Validation
AI tools rarely generate code that validates the actual content of uploaded files:
// Example of missing content validation in AI-generated code
app.post('/upload-profile-picture', upload.single('image'), (req, res) => {
const file = req.file;
// Vulnerable: Only checking MIME type without content validation
if (!file.mimetype.startsWith('image/')) {
return res.status(400).json({ error: 'Only image files are allowed' });
}
// Process image file
// ...
res.json({ message: 'Profile picture uploaded successfully' });
});
This approach is vulnerable because:
- It doesn’t verify that the file actually contains valid image data
- It allows uploading of malicious files with spoofed MIME types
- It doesn’t check for malicious content within valid file formats
- It creates a false sense of security
Insecure File Processing
AI-generated code often includes insecure file processing operations:
// Example of insecure file processing in AI-generated code
app.post('/upload-csv', upload.single('csv'), (req, res) => {
const file = req.file;
// Vulnerable: Using child_process to process uploaded file
exec(`python process_csv.py ${file.path}`, (error, stdout, stderr) => {
if (error) {
return res.status(500).json({ error: 'Failed to process CSV' });
}
res.json({
message: 'CSV processed successfully',
result: stdout
});
});
});
This code is vulnerable because:
- It uses command-line execution with user-controlled input
- It enables command injection attacks
- It doesn’t validate the file before processing
- It exposes sensitive error information
Platform-Specific File Upload Vulnerabilities
Each full-stack app builder has its own patterns of file handling implementation. Let’s examine specific examples from each platform.
Lovable.dev
Lovable.dev integrates with Supabase for storage, but AI-generated code might not implement proper security measures:
// Lovable.dev vulnerable file upload implementation
import { supabase } from '../lib/supabaseClient';
// API route for file upload
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Vulnerable: No authentication check
// Parse multipart form data
const form = new formidable.IncomingForm();
form.parse(req, async (err, fields, files) => {
if (err) {
return res.status(500).json({ error: 'Failed to parse form' });
}
const file = files.file;
// Vulnerable: Insufficient file validation
if (!file.mimetype.startsWith('image/')) {
return res.status(400).json({ error: 'Only image files are allowed' });
}
// Read file content
const fileContent = await fs.promises.readFile(file.filepath);
// Vulnerable: Using original filename
const fileName = file.originalFilename;
// Upload to Supabase Storage
const { data, error } = await supabase
.storage
.from('images')
.upload(`public/${fileName}`, fileContent, {
contentType: file.mimetype,
upsert: true // Vulnerable: Allows overwriting existing files
});
if (error) {
return res.status(500).json({ error: error.message });
}
// Vulnerable: Returning direct file URL
const fileUrl = supabase.storage.from('images').getPublicUrl(`public/${fileName}`).data.publicUrl;
heroImage: "/images/blog/secure-file-handling-and-upload-security-in-vibe-coded-applications.jpg"
});
} catch (error) {
return res.status(500).json({ error: 'Internal server error' });
}
}
The issues here include:
- No authentication or authorization checks
- Insufficient file type validation
- Using original filenames without sanitization
- Allowing file overwriting
- No file size restrictions
- Returning direct file URLs
Bolt.new
Bolt.new might generate TypeScript code with file upload vulnerabilities:
// Bolt.new vulnerable file upload implementation
import type { NextApiRequest, NextApiResponse } from 'next';
import { IncomingForm } from 'formidable';
import { promises as fs } from 'fs';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
export const config = {
api: {
bodyParser: false,
},
};
type ResponseData = {
url?: string;
error?: string;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Vulnerable: No authentication check
const form = new IncomingForm({
// Vulnerable: No file size limit
// Vulnerable: Uploading to public directory
uploadDir: path.join(process.cwd(), 'public/uploads'),
keepExtensions: true,
});
form.parse(req, async (err, fields, files) => {
if (err) {
return res.status(500).json({ error: 'Failed to parse form' });
}
const file = files.file as any;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Vulnerable: Insufficient file type validation
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (!allowedTypes.includes(file.mimetype)) {
// Remove the uploaded file
await fs.unlink(file.filepath);
return res.status(400).json({ error: 'Invalid file type' });
}
// Vulnerable: Using UUID + original filename
const fileName = `${uuidv4()}-${file.originalFilename}`;
const newPath = path.join(process.cwd(), 'public/uploads', fileName);
// Rename the file
await fs.rename(file.filepath, newPath);
// Vulnerable: Returning direct file URL
heroImage: "/images/blog/secure-file-handling-and-upload-security-in-vibe-coded-applications.jpg"
});
} catch (error) {
return res.status(500).json({ error: 'Internal server error' });
}
}
The vulnerabilities include:
- No authentication or authorization checks
- Insufficient file type validation
- Storing files in a public directory
- No file size limits
- Partial filename sanitization (still includes original filename)
- Returning direct file URLs
Tempo Labs
Tempo Labs might generate code that uses cloud storage but with security gaps:
// Tempo Labs vulnerable file upload implementation
import { PrismaClient } from '@prisma/client';
import { Storage } from '@google-cloud/storage';
import multer from 'multer';
import { v4 as uuidv4 } from 'uuid';
const prisma = new PrismaClient();
const storage = new Storage();
const bucket = storage.bucket(process.env.GCS_BUCKET_NAME);
// Configure multer
const upload = multer({
storage: multer.memoryStorage(),
// Vulnerable: No file size limits
});
// API route for document upload
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Vulnerable: Basic token verification without proper checks
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Authentication required' });
}
// Process file upload
upload.single('document')(req, res, async (err) => {
if (err) {
return res.status(500).json({ error: 'Upload failed' });
}
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Vulnerable: Insufficient file type validation
const allowedTypes = ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
if (!allowedTypes.includes(file.mimetype)) {
return res.status(400).json({ error: 'Invalid file type' });
}
// Vulnerable: Using original filename in the generated name
const fileName = `${uuidv4()}-${file.originalname}`;
// Create a new blob in the bucket
const blob = bucket.file(fileName);
const blobStream = blob.createWriteStream({
resumable: false,
// Vulnerable: Public access by default
public: true,
});
blobStream.on('error', (err) => {
return res.status(500).json({ error: 'Failed to upload file' });
});
blobStream.on('finish', async () => {
// Vulnerable: Storing file metadata without user association
// Vulnerable: Returning public URL
const publicUrl = `https://storage.googleapis.com/${bucket.name}/${blob.name}`;
// Store file reference in database
const document = await prisma.document.create({
data: {
name: file.originalname,
type: file.mimetype,
size: file.size,
heroImage: "/images/blog/secure-file-handling-and-upload-security-in-vibe-coded-applications.jpg"
// Vulnerable: No user association
},
});
res.status(200).json({
message: 'File uploaded successfully',
document,
});
});
blobStream.end(file.buffer);
});
} catch (error) {
return res.status(500).json({ error: 'Internal server error' });
}
}
The security issues include:
- Weak authentication
- Insufficient file type validation
- No file size limits
- Using original filenames in the generated name
- Public access to uploaded files by default
- No proper user association for uploaded files
- Returning public URLs
Base44
Base44 often generates more bare-bones file upload code with significant security gaps:
// Base44 vulnerable file upload implementation
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const router = express.Router();
// Configure storage
const storage = multer.diskStorage({
destination: function (req, file, cb) {
// Vulnerable: Storing in public directory
cb(null, 'public/uploads/');
},
filename: function (req, file, cb) {
// Vulnerable: Using timestamp + original filename
cb(null, Date.now() + '-' + file.originalname);
}
});
// Configure upload middleware
const upload = multer({
storage: storage,
// Vulnerable: No file size limits
// Vulnerable: Basic file filter
fileFilter: function (req, file, cb) {
const filetypes = /jpeg|jpg|png|gif/;
const mimetype = filetypes.test(file.mimetype);
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
if (mimetype && extname) {
return cb(null, true);
}
cb(new Error('Only image files are allowed'));
}
});
// File upload route
router.post('/upload', upload.single('image'), (req, res) => {
// Vulnerable: No authentication check
try {
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Vulnerable: Returning direct file path
const filePath = '/uploads/' + file.filename;
// Vulnerable: Storing file reference without user association
const fileData = {
filename: file.filename,
originalname: file.originalname,
mimetype: file.mimetype,
size: file.size,
path: filePath
};
// Vulnerable: Storing file metadata in a JSON file
const dbPath = path.join(__dirname, '../data/uploads.json');
let uploads = [];
if (fs.existsSync(dbPath)) {
const data = fs.readFileSync(dbPath, 'utf8');
uploads = JSON.parse(data);
}
uploads.push(fileData);
fs.writeFileSync(dbPath, JSON.stringify(uploads, null, 2));
res.status(200).json({
message: 'File uploaded successfully',
file: fileData
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: 'File upload failed' });
}
});
// Get uploaded files
router.get('/uploads', (req, res) => {
// Vulnerable: No authentication check
try {
const dbPath = path.join(__dirname, '../data/uploads.json');
if (!fs.existsSync(dbPath)) {
return res.status(200).json({ uploads: [] });
}
const data = fs.readFileSync(dbPath, 'utf8');
const uploads = JSON.parse(data);
// Vulnerable: Returning all file metadata
res.status(200).json({ uploads });
} catch (error) {
console.error('Error fetching uploads:', error);
res.status(500).json({ error: 'Failed to fetch uploads' });
}
});
module.exports = router;
The security issues include:
- No authentication or authorization checks
- Storing files in a public directory
- Using timestamp + original filename
- Basic file type validation
- No file size limits
- Returning direct file paths
- No user association for uploaded files
- Storing file metadata in an insecure JSON file
- Exposing all file metadata to any user
Replit
Replit’s AI assistant often generates Python file upload code with security weaknesses:
# Replit vulnerable file upload implementation
from flask import Flask, request, jsonify, send_from_directory
import os
import uuid
from werkzeug.utils import secure_filename
import json
app = Flask(__name__)
# Configure upload settings
UPLOAD_FOLDER = 'uploads'
ALLOWED_EXTENSIONS = {'txt', 'pdf', 'png', 'jpg', 'jpeg', 'gif'}
# Vulnerable: No file size limit
# Create upload directory if it doesn't exist
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
@app.route('/upload', methods=['POST'])
def upload_file():
# Vulnerable: No authentication check
# Check if file part exists
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400
file = request.files['file']
# Check if file is selected
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
if file and allowed_file(file.filename):
# Vulnerable: Using secure_filename but still including original filename
filename = secure_filename(file.filename)
# Add UUID to prevent filename collisions
unique_filename = f"{uuid.uuid4()}_{filename}"
file_path = os.path.join(UPLOAD_FOLDER, unique_filename)
# Save the file
file.save(file_path)
# Vulnerable: Storing file metadata without user association
file_data = {
'original_name': filename,
'saved_name': unique_filename,
'path': file_path,
'type': file.content_type,
'size': os.path.getsize(file_path)
}
# Vulnerable: Storing metadata in a JSON file
metadata_path = 'uploads/metadata.json'
metadata = []
if os.path.exists(metadata_path):
with open(metadata_path, 'r') as f:
metadata = json.load(f)
metadata.append(file_data)
with open(metadata_path, 'w') as f:
json.dump(metadata, f, indent=2)
# Vulnerable: Returning direct file path
return jsonify({
'message': 'File uploaded successfully',
'file': file_data
}), 200
return jsonify({'error': 'File type not allowed'}), 400
@app.route('/uploads/<filename>', methods=['GET'])
def get_file(filename):
# Vulnerable: No authentication or authorization check
return send_from_directory(UPLOAD_FOLDER, filename)
@app.route('/files', methods=['GET'])
def list_files():
# Vulnerable: No authentication check
metadata_path = 'uploads/metadata.json'
if not os.path.exists(metadata_path):
return jsonify({'files': []}), 200
with open(metadata_path, 'r') as f:
metadata = json.load(f)
# Vulnerable: Returning all file metadata
return jsonify({'files': metadata}), 200
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080, debug=True)
The security issues include:
- No authentication or authorization checks
- Using secure_filename but still including original filename
- No file size limits
- Basic file type validation
- Storing files in a directory accessible via a direct route
- No user association for uploaded files
- Storing file metadata in an insecure JSON file
- Exposing all file metadata to any user
- Running with debug mode enabled
Secure File Upload Implementation Techniques
Now that we’ve identified common vulnerabilities, let’s explore secure implementation techniques for file uploads in vibe-coded applications.
Comprehensive File Type Validation
Implement thorough file type validation that goes beyond MIME types:
// Secure file type validation
const fileTypeChecker = require('file-type-checker');
const fs = require('fs');
async function validateFileType(filePath, allowedTypes) {
// Check file signature (magic bytes)
const fileType = await fileTypeChecker.detectFile(filePath);
if (!fileType || !allowedTypes.includes(fileType.mime)) {
return false;
}
// Additional validation for specific file types
if (fileType.mime.startsWith('image/')) {
// Validate image dimensions
const dimensions = await getImageDimensions(filePath);
if (dimensions.width > 5000 || dimensions.height > 5000) {
return false; // Reject suspiciously large images
}
}
return true;
}
// Example usage in upload handler
app.post('/upload', upload.single('file'), async (req, res) => {
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
// Validate file type
const isValidType = await validateFileType(file.path, allowedTypes);
if (!isValidType) {
// Remove the invalid file
fs.unlinkSync(file.path);
return res.status(400).json({ error: 'Invalid file type' });
}
// Continue with secure file processing
// ...
});
This approach:
- Checks file signatures (magic bytes) instead of relying on MIME types
- Performs additional validation for specific file types
- Rejects files that don’t match the expected format
- Removes invalid files to prevent storage of malicious content
Secure Filename Handling
Implement secure filename handling to prevent path traversal and other attacks:
// Secure filename handling
const crypto = require('crypto');
const path = require('path');
function generateSecureFilename(originalFilename) {
// Generate a random filename
const randomString = crypto.randomBytes(16).toString('hex');
// Extract and validate the extension
let extension = '';
if (originalFilename.includes('.')) {
extension = originalFilename.split('.').pop().toLowerCase();
// Validate extension (whitelist approach)
const allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'];
if (!allowedExtensions.includes(extension)) {
extension = 'bin'; // Default to binary for unknown extensions
}
} else {
extension = 'bin'; // Default to binary for files without extension
}
// Create secure filename
return `${randomString}.${extension}`;
}
// Example usage in upload handler
app.post('/upload', upload.single('file'), (req, res) => {
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Generate secure filename
const secureFilename = generateSecureFilename(file.originalname);
// Move file to permanent storage with secure filename
const destinationPath = path.join(uploadDir, secureFilename);
fs.renameSync(file.path, destinationPath);
// Store original filename and secure filename mapping in database
// ...
res.json({
message: 'File uploaded successfully',
fileId: fileId // Return a reference ID, not the actual filename
});
});
This approach:
- Generates random filenames to prevent path traversal
- Validates and normalizes file extensions
- Stores the mapping between original and secure filenames
- Returns a reference ID instead of the actual filename
Implement File Size Restrictions
Add file size restrictions to prevent denial of service attacks:
// Secure file size restrictions
const multer = require('multer');
const path = require('path');
// Configure storage
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'temp-uploads/');
},
filename: function (req, file, cb) {
// Temporary filename for initial storage
const tempFilename = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
cb(null, tempFilename);
}
});
// Configure upload middleware with size limits
const upload = multer({
storage: storage,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB limit
files: 1 // Only allow one file per request
},
fileFilter: function (req, file, cb) {
// Basic file type check (will be enhanced later)
const filetypes = /jpeg|jpg|png|gif/;
const mimetype = filetypes.test(file.mimetype);
const extname = filetypes.test(path.extname(file.originalname).toLowerCase());
if (mimetype && extname) {
return cb(null, true);
}
cb(new Error('Only image files are allowed'));
}
});
// Example usage in upload handler
app.post('/upload', function (req, res) {
upload.single('image')(req, res, function (err) {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File too large (max: 5MB)' });
}
return res.status(400).json({ error: 'Upload error' });
} else if (err) {
return res.status(400).json({ error: err.message });
}
// Continue with secure file processing
// ...
});
});
This approach:
- Sets a maximum file size limit
- Limits the number of files per request
- Provides clear error messages for size violations
- Uses a temporary storage location for initial upload
Secure File Storage
Implement secure file storage practices:
// Secure file storage
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
// Storage directories
const TEMP_UPLOAD_DIR = path.join(__dirname, 'temp-uploads');
const PRIVATE_STORAGE_DIR = path.join(__dirname, 'private-storage');
// Ensure directories exist
fs.mkdirSync(TEMP_UPLOAD_DIR, { recursive: true });
fs.mkdirSync(PRIVATE_STORAGE_DIR, { recursive: true });
// Store file securely
async function securelyStoreFile(tempFilePath, userId) {
// Generate secure random filename
const randomFilename = crypto.randomBytes(32).toString('hex');
// Create user-specific subdirectory
const userDir = path.join(PRIVATE_STORAGE_DIR, userId.toString());
fs.mkdirSync(userDir, { recursive: true });
// Move file to secure location
const destinationPath = path.join(userDir, randomFilename);
await fs.promises.rename(tempFilePath, destinationPath);
// Return the relative path for database storage
return path.join(userId.toString(), randomFilename);
}
// Example usage in upload handler
app.post('/upload', authenticateUser, upload.single('file'), async (req, res) => {
try {
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Validate file type (implementation from previous section)
const isValidType = await validateFileType(file.path, ['image/jpeg', 'image/png']);
if (!isValidType) {
// Remove the invalid file
await fs.promises.unlink(file.path);
return res.status(400).json({ error: 'Invalid file type' });
}
// Store file securely
const relativePath = await securelyStoreFile(file.path, req.user.id);
// Store file metadata in database
const fileId = await storeFileMetadata({
userId: req.user.id,
originalName: file.originalname,
storagePath: relativePath,
mimeType: file.mimetype,
size: file.size
});
res.json({
message: 'File uploaded successfully',
fileId: fileId
});
} catch (error) {
console.error('Upload error:', error);
res.status(500).json({ error: 'File upload failed' });
}
});
This approach:
- Stores files in a private directory outside the web root
- Uses user-specific subdirectories for isolation
- Generates secure random filenames
- Associates files with specific users
- Returns only a reference ID, not the actual file path
Implement Secure File Serving
Create a secure mechanism for serving uploaded files:
// Secure file serving
const fs = require('fs');
const path = require('path');
// Serve file securely
app.get('/files/:fileId', authenticateUser, async (req, res) => {
try {
const fileId = req.params.fileId;
// Retrieve file metadata from database
const fileMetadata = await getFileMetadata(fileId);
if (!fileMetadata) {
return res.status(404).json({ error: 'File not found' });
}
// Check if user has permission to access this file
if (fileMetadata.userId !== req.user.id && !req.user.isAdmin) {
return res.status(403).json({ error: 'Access denied' });
}
// Construct full file path
const filePath = path.join(PRIVATE_STORAGE_DIR, fileMetadata.storagePath);
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File not found' });
}
// Set appropriate content type
res.setHeader('Content-Type', fileMetadata.mimeType);
// Set content disposition to prevent browser execution
res.setHeader('Content-Disposition', `inline; filename="${fileMetadata.originalName}"`);
// Set security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
// Stream file to response
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
} catch (error) {
console.error('File serving error:', error);
res.status(500).json({ error: 'Failed to serve file' });
}
});
This approach:
- Authenticates users before serving files
- Verifies that the user has permission to access the file
- Sets appropriate content type and security headers
- Uses content disposition to control how browsers handle the file
- Streams the file instead of loading it entirely into memory
Implement Content Validation
Add content validation for specific file types:
// Secure content validation
const sharp = require('sharp');
const pdf = require('pdf-parse');
const fs = require('fs');
// Validate image content
async function validateImageContent(filePath) {
try {
// Try to process the image with sharp
const metadata = await sharp(filePath).metadata();
// Check for suspicious characteristics
if (metadata.width > 8000 || metadata.height > 8000) {
return { valid: false, reason: 'Image dimensions too large' };
}
if (metadata.size > 15 * 1024 * 1024) { // 15MB
return { valid: false, reason: 'Image file too large' };
}
// Additional checks could be added here
return { valid: true };
} catch (error) {
return { valid: false, reason: 'Invalid image format' };
}
}
// Validate PDF content
async function validatePdfContent(filePath) {
try {
// Read the PDF file
const dataBuffer = fs.readFileSync(filePath);
// Try to parse the PDF
const data = await pdf(dataBuffer);
// Check for suspicious characteristics
if (data.numpages > 1000) {
return { valid: false, reason: 'Too many pages' };
}
// Check for JavaScript (potential security risk)
if (data.text.includes('/JS') || data.text.includes('/JavaScript')) {
return { valid: false, reason: 'PDF contains JavaScript' };
}
return { valid: true };
} catch (error) {
return { valid: false, reason: 'Invalid PDF format' };
}
}
// Example usage in upload handler
app.post('/upload', authenticateUser, upload.single('file'), async (req, res) => {
try {
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Validate file type (implementation from previous section)
const fileType = await fileTypeChecker.detectFile(file.path);
if (!fileType) {
await fs.promises.unlink(file.path);
return res.status(400).json({ error: 'Could not determine file type' });
}
// Validate content based on file type
let contentValidation;
if (fileType.mime.startsWith('image/')) {
contentValidation = await validateImageContent(file.path);
} else if (fileType.mime === 'application/pdf') {
contentValidation = await validatePdfContent(file.path);
} else {
await fs.promises.unlink(file.path);
return res.status(400).json({ error: 'Unsupported file type' });
}
if (!contentValidation.valid) {
await fs.promises.unlink(file.path);
return res.status(400).json({
error: 'Invalid file content',
reason: contentValidation.reason
});
}
// Continue with secure file processing
// ...
} catch (error) {
console.error('Upload error:', error);
// Clean up temporary file if it exists
if (req.file && req.file.path) {
try {
await fs.promises.unlink(req.file.path);
} catch (unlinkError) {
console.error('Failed to remove temporary file:', unlinkError);
}
}
res.status(500).json({ error: 'File upload failed' });
}
});
This approach:
- Validates the actual content of uploaded files
- Performs format-specific validation
- Checks for suspicious characteristics
- Rejects files with potentially malicious content
- Cleans up temporary files in case of validation failure
Platform-Specific Secure File Upload Implementations
Let’s look at secure implementations for each full-stack app builder.
Lovable.dev Secure Implementation
For Lovable.dev’s Supabase-based applications:
// Secure Lovable.dev file upload implementation
import { createServerSupabaseClient } from '@supabase/auth-helpers-nextjs';
import formidable from 'formidable';
import { v4 as uuidv4 } from 'uuid';
import fs from 'fs';
import path from 'path';
import fileTypeChecker from 'file-type-checker';
// Disable body parsing for file uploads
export const config = {
api: {
bodyParser: false,
},
};
// Validate file type
async function validateFileType(filePath, allowedTypes) {
try {
// Check file signature
const fileType = await fileTypeChecker.detectFile(filePath);
if (!fileType || !allowedTypes.includes(fileType.mime)) {
return false;
}
return true;
} catch (error) {
console.error('File type validation error:', error);
return false;
}
}
// API route for file upload
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Create Supabase client with auth context
const supabase = createServerSupabaseClient({ req, res });
// Check if user is authenticated
const { data: { session }, error: authError } = await supabase.auth.getSession();
if (authError || !session) {
return res.status(401).json({ error: 'Authentication required' });
}
// Get user ID
const userId = session.user.id;
// Configure form parser
const form = new formidable.IncomingForm({
maxFileSize: 5 * 1024 * 1024, // 5MB limit
keepExtensions: true,
uploadDir: path.join(process.cwd(), 'tmp')
});
// Parse form
form.parse(req, async (err, fields, files) => {
if (err) {
if (err.code === 1009) { // File size limit exceeded
return res.status(400).json({ error: 'File too large (max: 5MB)' });
}
return res.status(500).json({ error: 'Failed to parse form' });
}
const file = files.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Validate file type
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
const isValidType = await validateFileType(file.filepath, allowedTypes);
if (!isValidType) {
// Remove invalid file
await fs.promises.unlink(file.filepath);
return res.status(400).json({ error: 'Invalid file type' });
}
// Generate secure filename
const fileExtension = path.extname(file.originalFilename).toLowerCase();
const secureFilename = `${userId}/${uuidv4()}${fileExtension}`;
// Read file content
const fileContent = await fs.promises.readFile(file.filepath);
// Upload to Supabase Storage with private access
const { data, error } = await supabase
.storage
.from('user-files')
.upload(secureFilename, fileContent, {
contentType: file.mimetype,
upsert: false // Prevent overwriting
});
// Remove temporary file
await fs.promises.unlink(file.filepath);
if (error) {
return res.status(500).json({ error: 'Failed to store file' });
}
// Store file metadata in database
const { data: fileData, error: dbError } = await supabase
.from('files')
.insert({
user_id: userId,
original_name: file.originalFilename,
storage_path: secureFilename,
mime_type: file.mimetype,
size: file.size
})
.select('id')
.single();
if (dbError) {
// If database insert fails, try to delete the uploaded file
await supabase.storage.from('user-files').remove([secureFilename]);
return res.status(500).json({ error: 'Failed to store file metadata' });
}
// Return file ID, not the URL
return res.status(200).json({
message: 'File uploaded successfully',
fileId: fileData.id
});
});
} catch (error) {
console.error('Upload error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
// Secure file serving route
export async function getFileHandler(req, res) {
const { fileId } = req.query;
try {
// Create Supabase client with auth context
const supabase = createServerSupabaseClient({ req, res });
// Check if user is authenticated
const { data: { session }, error: authError } = await supabase.auth.getSession();
if (authError || !session) {
return res.status(401).json({ error: 'Authentication required' });
}
// Get user ID
const userId = session.user.id;
// Get file metadata
const { data: fileData, error: fileError } = await supabase
.from('files')
.select('*')
.eq('id', fileId)
.single();
if (fileError || !fileData) {
return res.status(404).json({ error: 'File not found' });
}
// Check if user has access to this file
if (fileData.user_id !== userId) {
// Check if user has admin role
const { data: roleData } = await supabase
.from('user_roles')
.select('role')
.eq('user_id', userId)
.single();
if (!roleData || roleData.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
}
// Generate signed URL with short expiration
const { data: { signedURL }, error: signedUrlError } = await supabase
.storage
.from('user-files')
.createSignedUrl(fileData.storage_path, 60); // 60 seconds expiration
if (signedUrlError) {
return res.status(500).json({ error: 'Failed to generate file access URL' });
}
// Redirect to signed URL
return res.redirect(signedURL);
} catch (error) {
console.error('File serving error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
This implementation:
- Authenticates users before allowing uploads
- Limits file size to 5MB
- Validates file types using file signatures
- Generates secure filenames with UUID
- Stores files with user-specific paths
- Uses private storage buckets
- Associates files with users in the database
- Generates short-lived signed URLs for file access
- Implements proper authorization for file access
Bolt.new Secure Implementation
For Bolt.new’s Next.js applications:
// Secure Bolt.new file upload implementation
import type { NextApiRequest, NextApiResponse } from 'next';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]';
import { IncomingForm } from 'formidable';
import { promises as fs } from 'fs';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { PrismaClient } from '@prisma/client';
import fileTypeChecker from 'file-type-checker';
const prisma = new PrismaClient();
export const config = {
api: {
bodyParser: false,
},
};
// Allowed file types
const ALLOWED_FILE_TYPES = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'application/pdf': 'pdf'
};
// Max file size (5MB)
const MAX_FILE_SIZE = 5 * 1024 * 1024;
// Storage directories
const TEMP_DIR = path.join(process.cwd(), 'tmp');
const STORAGE_DIR = path.join(process.cwd(), 'private-storage');
// Ensure directories exist
fs.mkdir(TEMP_DIR, { recursive: true });
fs.mkdir(STORAGE_DIR, { recursive: true });
// Validate file type
async function validateFileType(filePath: string): Promise<boolean> {
try {
const fileType = await fileTypeChecker.detectFile(filePath);
if (!fileType || !Object.keys(ALLOWED_FILE_TYPES).includes(fileType.mime)) {
return false;
}
return true;
} catch (error) {
console.error('File type validation error:', error);
return false;
}
}
type ResponseData = {
fileId?: string;
message?: string;
error?: string;
};
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Check authentication
const session = await getServerSession(req, res, authOptions);
if (!session || !session.user) {
return res.status(401).json({ error: 'Authentication required' });
}
// Get user from database
const user = await prisma.user.findUnique({
where: { email: session.user.email as string }
});
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
// Configure form parser
const form = new IncomingForm({
maxFileSize: MAX_FILE_SIZE,
uploadDir: TEMP_DIR,
keepExtensions: true,
});
// Parse form
form.parse(req, async (err, fields, files) => {
if (err) {
if (err.message.includes('maxFileSize')) {
return res.status(400).json({ error: 'File too large (max: 5MB)' });
}
return res.status(500).json({ error: 'Failed to parse form' });
}
const file = files.file as any;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Validate file type
const isValidType = await validateFileType(file.filepath);
if (!isValidType) {
// Remove invalid file
await fs.unlink(file.filepath);
return res.status(400).json({ error: 'Invalid file type' });
}
try {
// Create user directory if it doesn't exist
const userDir = path.join(STORAGE_DIR, user.id);
await fs.mkdir(userDir, { recursive: true });
// Generate secure filename
const fileId = uuidv4();
const fileType = await fileTypeChecker.detectFile(file.filepath);
const extension = ALLOWED_FILE_TYPES[fileType.mime];
const secureFilename = `${fileId}.${extension}`;
const storagePath = path.join(userDir, secureFilename);
// Move file to secure storage
await fs.rename(file.filepath, storagePath);
// Store file metadata in database
const fileRecord = await prisma.file.create({
data: {
id: fileId,
userId: user.id,
originalName: file.originalFilename,
storagePath: path.relative(STORAGE_DIR, storagePath),
mimeType: fileType.mime,
size: file.size,
uploadedAt: new Date()
}
});
return res.status(200).json({
message: 'File uploaded successfully',
fileId: fileRecord.id
});
} catch (error) {
// Clean up temporary file if it still exists
try {
await fs.unlink(file.filepath);
} catch (unlinkError) {
// File might have been moved already
}
console.error('File processing error:', error);
return res.status(500).json({ error: 'Failed to process file' });
}
});
} catch (error) {
console.error('Upload error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
// Secure file serving endpoint
export async function getFileHandler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { fileId } = req.query;
if (!fileId || typeof fileId !== 'string') {
return res.status(400).json({ error: 'Invalid file ID' });
}
try {
// Check authentication
const session = await getServerSession(req, res, authOptions);
if (!session || !session.user) {
return res.status(401).json({ error: 'Authentication required' });
}
// Get user from database
const user = await prisma.user.findUnique({
where: { email: session.user.email as string }
});
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
// Get file metadata
const file = await prisma.file.findUnique({
where: { id: fileId }
});
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// Check if user has access to this file
if (file.userId !== user.id) {
// Check if user has admin role
const userRole = await prisma.userRole.findUnique({
where: { userId: user.id }
});
if (!userRole || userRole.role !== 'ADMIN') {
return res.status(403).json({ error: 'Access denied' });
}
}
// Construct full file path
const filePath = path.join(STORAGE_DIR, file.storagePath);
// Check if file exists
try {
await fs.access(filePath);
} catch (error) {
return res.status(404).json({ error: 'File not found' });
}
// Set appropriate headers
res.setHeader('Content-Type', file.mimeType);
res.setHeader('Content-Disposition', `inline; filename="${file.originalName}"`);
res.setHeader('X-Content-Type-Options', 'nosniff');
// Stream file to response
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
} catch (error) {
console.error('File serving error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
This implementation:
- Uses Next.js authentication
- Limits file size to 5MB
- Validates file types using file signatures
- Generates secure filenames with UUID
- Stores files in a private directory with user-specific paths
- Associates files with users in the database
- Implements proper authorization for file access
- Sets appropriate headers when serving files
Tempo Labs Secure Implementation
For Tempo Labs’ applications:
// Secure Tempo Labs file upload implementation
import { PrismaClient } from '@prisma/client';
import { Storage } from '@google-cloud/storage';
import multer from 'multer';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
import fs from 'fs';
import os from 'os';
import fileTypeChecker from 'file-type-checker';
import { verify } from 'jsonwebtoken';
const prisma = new PrismaClient();
// Initialize Google Cloud Storage
const storage = new Storage({
keyFilename: process.env.GCS_KEY_FILE
});
const bucket = storage.bucket(process.env.GCS_BUCKET_NAME);
// Allowed file types
const ALLOWED_FILE_TYPES = {
'application/pdf': 'pdf',
'application/msword': 'doc',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx'
};
// Configure multer for temporary storage
const upload = multer({
storage: multer.diskStorage({
destination: function (req, file, cb) {
const tempDir = path.join(os.tmpdir(), 'uploads');
fs.mkdirSync(tempDir, { recursive: true });
cb(null, tempDir);
},
filename: function (req, file, cb) {
const tempFilename = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
cb(null, tempFilename);
}
}),
limits: {
fileSize: 10 * 1024 * 1024, // 10MB limit
files: 1
}
});
// Validate file type
async function validateFileType(filePath, allowedTypes) {
try {
const fileType = await fileTypeChecker.detectFile(filePath);
if (!fileType || !Object.keys(allowedTypes).includes(fileType.mime)) {
return { valid: false, type: null };
}
return { valid: true, type: fileType.mime };
} catch (error) {
console.error('File type validation error:', error);
return { valid: false, type: null };
}
}
// Validate PDF content
async function validatePdfContent(filePath) {
try {
const pdf = require('pdf-parse');
const dataBuffer = fs.readFileSync(filePath);
// Try to parse the PDF
const data = await pdf(dataBuffer);
// Check for suspicious characteristics
if (data.numpages > 100) {
return { valid: false, reason: 'Too many pages' };
}
// Check for JavaScript (potential security risk)
if (data.text.includes('/JS') || data.text.includes('/JavaScript')) {
return { valid: false, reason: 'PDF contains JavaScript' };
}
return { valid: true };
} catch (error) {
return { valid: false, reason: 'Invalid PDF format' };
}
}
// Authentication middleware
async function authenticate(req, res, next) {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
const token = authHeader.split(' ')[1];
// Verify token
const decoded = verify(token, process.env.JWT_SECRET);
// Get user from database
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, role: true, active: true }
});
if (!user || !user.active) {
return res.status(401).json({ error: 'Invalid or inactive user' });
}
req.user = user;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// API route for document upload
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
// Authenticate user
await authenticate(req, res, async () => {
// Process file upload
upload.single('document')(req, res, async (err) => {
if (err) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File too large (max: 10MB)' });
}
return res.status(500).json({ error: 'Upload failed' });
}
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
try {
// Validate file type
const typeValidation = await validateFileType(file.path, ALLOWED_FILE_TYPES);
if (!typeValidation.valid) {
await fs.promises.unlink(file.path);
return res.status(400).json({ error: 'Invalid file type' });
}
// Additional content validation for PDFs
if (typeValidation.type === 'application/pdf') {
const contentValidation = await validatePdfContent(file.path);
if (!contentValidation.valid) {
await fs.promises.unlink(file.path);
return res.status(400).json({
error: 'Invalid file content',
reason: contentValidation.reason
});
}
}
// Generate secure filename
const fileId = uuidv4();
const extension = ALLOWED_FILE_TYPES[typeValidation.type];
const secureFilename = `${req.user.id}/${fileId}.${extension}`;
// Upload to Google Cloud Storage with private access
await bucket.upload(file.path, {
destination: secureFilename,
metadata: {
contentType: typeValidation.type,
metadata: {
originalName: file.originalname,
userId: req.user.id
}
}
});
// Remove temporary file
await fs.promises.unlink(file.path);
// Make file private (ensure it's not publicly accessible)
await bucket.file(secureFilename).makePrivate();
// Store file metadata in database
const document = await prisma.document.create({
data: {
id: fileId,
userId: req.user.id,
originalName: file.originalname,
storagePath: secureFilename,
mimeType: typeValidation.type,
size: file.size,
uploadedAt: new Date()
}
});
res.status(200).json({
message: 'File uploaded successfully',
documentId: document.id
});
} catch (error) {
console.error('File processing error:', error);
// Clean up temporary file if it still exists
try {
await fs.promises.unlink(file.path);
} catch (unlinkError) {
// File might have been deleted already
}
return res.status(500).json({ error: 'Failed to process file' });
}
});
});
}
// Secure file serving endpoint
export async function getDocumentHandler(req, res) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const { documentId } = req.query;
if (!documentId) {
return res.status(400).json({ error: 'Document ID is required' });
}
try {
// Authenticate user
await authenticate(req, res, async () => {
// Get document metadata
const document = await prisma.document.findUnique({
where: { id: documentId }
});
if (!document) {
return res.status(404).json({ error: 'Document not found' });
}
// Check if user has access to this document
if (document.userId !== req.user.id) {
// Check if user has admin role
if (req.user.role !== 'ADMIN') {
return res.status(403).json({ error: 'Access denied' });
}
}
// Generate signed URL with short expiration
const [signedUrl] = await bucket.file(document.storagePath).getSignedUrl({
version: 'v4',
action: 'read',
expires: Date.now() + 5 * 60 * 1000 // 5 minutes
});
// Redirect to signed URL
res.redirect(signedUrl);
});
} catch (error) {
console.error('Document serving error:', error);
return res.status(500).json({ error: 'Internal server error' });
}
}
This implementation:
- Uses JWT authentication
- Limits file size to 10MB
- Validates file types using file signatures
- Performs additional content validation for PDFs
- Generates secure filenames with UUID
- Stores files in Google Cloud Storage with user-specific paths
- Makes files private (not publicly accessible)
- Associates files with users in the database
- Generates short-lived signed URLs for file access
- Implements proper authorization for file access
Base44 Secure Implementation
For Base44’s applications:
// Secure Base44 file upload implementation
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const crypto = require('crypto');
const fileTypeChecker = require('file-type-checker');
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
const { body, validationResult } = require('express-validator');
const router = express.Router();
// MongoDB models
const User = require('../models/User');
const File = require('../models/File');
// Storage directories
const TEMP_UPLOAD_DIR = path.join(__dirname, '../temp-uploads');
const PRIVATE_STORAGE_DIR = path.join(__dirname, '../private-storage');
// Ensure directories exist
fs.mkdirSync(TEMP_UPLOAD_DIR, { recursive: true });
fs.mkdirSync(PRIVATE_STORAGE_DIR, { recursive: true });
// Allowed file types
const ALLOWED_FILE_TYPES = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif'
};
// Configure multer for temporary storage
const upload = multer({
storage: multer.diskStorage({
destination: function (req, file, cb) {
cb(null, TEMP_UPLOAD_DIR);
},
filename: function (req, file, cb) {
const tempFilename = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
cb(null, tempFilename);
}
}),
limits: {
fileSize: 5 * 1024 * 1024, // 5MB limit
files: 1
}
});
// Validate file type
async function validateFileType(filePath) {
try {
const fileType = await fileTypeChecker.detectFile(filePath);
if (!fileType || !Object.keys(ALLOWED_FILE_TYPES).includes(fileType.mime)) {
return { valid: false, type: null };
}
return { valid: true, type: fileType.mime };
} catch (error) {
console.error('File type validation error:', error);
return { valid: false, type: null };
}
}
// Validate image content
async function validateImageContent(filePath) {
try {
const sharp = require('sharp');
const metadata = await sharp(filePath).metadata();
// Check for suspicious characteristics
if (metadata.width > 8000 || metadata.height > 8000) {
return { valid: false, reason: 'Image dimensions too large' };
}
return { valid: true };
} catch (error) {
return { valid: false, reason: 'Invalid image format' };
}
}
// Authentication middleware
function authenticate(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Authentication required' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid or expired token' });
}
}
// File upload route
router.post(
'/upload',
authenticate,
(req, res, next) => {
upload.single('image')(req, res, function (err) {
if (err) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: 'File too large (max: 5MB)' });
}
return res.status(400).json({ error: 'Upload error' });
}
next();
});
},
async (req, res) => {
try {
const file = req.file;
if (!file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Validate file type
const typeValidation = await validateFileType(file.path);
if (!typeValidation.valid) {
await fs.promises.unlink(file.path);
return res.status(400).json({ error: 'Invalid file type' });
}
// Validate image content
const contentValidation = await validateImageContent(file.path);
if (!contentValidation.valid) {
await fs.promises.unlink(file.path);
return res.status(400).json({
error: 'Invalid image content',
reason: contentValidation.reason
});
}
// Get user from database
const user = await User.findById(req.user.userId);
if (!user) {
await fs.promises.unlink(file.path);
return res.status(401).json({ error: 'User not found' });
}
// Create user directory if it doesn't exist
const userDir = path.join(PRIVATE_STORAGE_DIR, user._id.toString());
await fs.promises.mkdir(userDir, { recursive: true });
// Generate secure filename
const fileId = new mongoose.Types.ObjectId();
const extension = ALLOWED_FILE_TYPES[typeValidation.type];
const secureFilename = `${fileId}.${extension}`;
const storagePath = path.join(userDir, secureFilename);
// Move file to secure storage
await fs.promises.rename(file.path, storagePath);
// Create file record in database
const fileRecord = new File({
_id: fileId,
user: user._id,
originalName: file.originalname,
storagePath: path.relative(PRIVATE_STORAGE_DIR, storagePath),
mimeType: typeValidation.type,
size: file.size,
uploadedAt: new Date()
});
await fileRecord.save();
res.status(200).json({
message: 'File uploaded successfully',
fileId: fileRecord._id
});
} catch (error) {
console.error('Upload error:', error);
// Clean up temporary file if it still exists
if (req.file && req.file.path) {
try {
await fs.promises.unlink(req.file.path);
} catch (unlinkError) {
// File might have been moved already
}
}
res.status(500).json({ error: 'File upload failed' });
}
}
);
// Get file metadata
router.get(
'/files/:id',
authenticate,
async (req, res) => {
try {
const fileId = req.params.id;
// Validate ObjectId
if (!mongoose.Types.ObjectId.isValid(fileId)) {
return res.status(400).json({ error: 'Invalid file ID' });
}
// Get file metadata
const file = await File.findById(fileId);
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// Check if user has access to this file
if (file.user.toString() !== req.user.userId) {
// Check if user has admin role
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
}
// Return file metadata (without storage path)
res.json({
id: file._id,
originalName: file.originalName,
mimeType: file.mimeType,
size: file.size,
uploadedAt: file.uploadedAt
});
} catch (error) {
console.error('File metadata error:', error);
res.status(500).json({ error: 'Failed to get file metadata' });
}
}
);
// Serve file content
router.get(
'/files/:id/content',
authenticate,
async (req, res) => {
try {
const fileId = req.params.id;
// Validate ObjectId
if (!mongoose.Types.ObjectId.isValid(fileId)) {
return res.status(400).json({ error: 'Invalid file ID' });
}
// Get file metadata
const file = await File.findById(fileId);
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// Check if user has access to this file
if (file.user.toString() !== req.user.userId) {
// Check if user has admin role
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
}
// Construct full file path
const filePath = path.join(PRIVATE_STORAGE_DIR, file.storagePath);
// Check if file exists
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: 'File content not found' });
}
// Set appropriate headers
res.setHeader('Content-Type', file.mimeType);
res.setHeader('Content-Disposition', `inline; filename="${file.originalName}"`);
res.setHeader('X-Content-Type-Options', 'nosniff');
// Stream file to response
const fileStream = fs.createReadStream(filePath);
fileStream.pipe(res);
} catch (error) {
console.error('File serving error:', error);
res.status(500).json({ error: 'Failed to serve file' });
}
}
);
// Delete file
router.delete(
'/files/:id',
authenticate,
async (req, res) => {
try {
const fileId = req.params.id;
// Validate ObjectId
if (!mongoose.Types.ObjectId.isValid(fileId)) {
return res.status(400).json({ error: 'Invalid file ID' });
}
// Get file metadata
const file = await File.findById(fileId);
if (!file) {
return res.status(404).json({ error: 'File not found' });
}
// Check if user has access to this file
if (file.user.toString() !== req.user.userId) {
// Check if user has admin role
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
}
// Construct full file path
const filePath = path.join(PRIVATE_STORAGE_DIR, file.storagePath);
// Delete file from storage
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
}
// Delete file record from database
await File.findByIdAndDelete(fileId);
res.json({ message: 'File deleted successfully' });
} catch (error) {
console.error('File deletion error:', error);
res.status(500).json({ error: 'Failed to delete file' });
}
}
);
module.exports = router;
This implementation:
- Uses JWT authentication
- Limits file size to 5MB
- Validates file types using file signatures
- Performs additional content validation for images
- Generates secure filenames with MongoDB ObjectId
- Stores files in a private directory with user-specific paths
- Associates files with users in the database
- Implements proper authorization for file access
- Sets appropriate headers when serving files
- Provides endpoints for file metadata, content, and deletion
Replit Secure Implementation
For Replit’s Python Flask applications:
# Secure Replit file upload implementation
from flask import Flask, request, jsonify, send_file, g
from flask_cors import CORS
from werkzeug.utils import secure_filename
import os
import uuid
import json
import sqlite3
import jwt
import datetime
import magic
import shutil
from functools import wraps
from PIL import Image
import io
app = Flask(__name__)
app.config['SECRET_KEY'] = os.environ.get('JWT_SECRET_KEY', 'default-dev-key-change-in-production')
# Configure CORS
CORS(app, resources={
r"/api/*": {
"origins": [
"https://yourapplication.com",
"https://admin.yourapplication.com"
],
"methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
"allow_headers": ["Content-Type", "Authorization"]
}
})
# Storage directories
TEMP_UPLOAD_DIR = 'temp-uploads'
PRIVATE_STORAGE_DIR = 'private-storage'
# Ensure directories exist
os.makedirs(TEMP_UPLOAD_DIR, exist_ok=True)
os.makedirs(PRIVATE_STORAGE_DIR, exist_ok=True)
# Allowed file types
ALLOWED_FILE_TYPES = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/gif': 'gif',
'application/pdf': 'pdf'
}
# Maximum file size (5MB)
MAX_FILE_SIZE = 5 * 1024 * 1024
# Database setup
DATABASE = 'database.db'
def get_db():
db = getattr(g, '_database', None)
if db is None:
db = g._database = sqlite3.connect(DATABASE)
db.row_factory = sqlite3.Row
return db
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
# Initialize database
def init_db():
with app.app_context():
db = get_db()
with app.open_resource('schema.sql', mode='r') as f:
db.cursor().executescript(f.read())
db.commit()
# Authentication decorator
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token = None
# Get token from header
auth_header = request.headers.get('Authorization')
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
if not token:
return jsonify({'error': 'Authentication required'}), 401
try:
# Decode token
data = jwt.decode(
token,
app.config['SECRET_KEY'],
algorithms=['HS256']
)
# Get user from database
db = get_db()
user = db.execute(
'SELECT id, username, email, role, active FROM users WHERE id = ?',
(data['user_id'],)
).fetchone()
if not user or not user['active']:
return jsonify({'error': 'Invalid or inactive user'}), 401
# Store user in request context
g.user = {
'id': user['id'],
'username': user['username'],
'email': user['email'],
'role': user['role']
}
except jwt.ExpiredSignatureError:
return jsonify({'error': 'Token has expired'}), 401
except jwt.InvalidTokenError:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated
# Role-based authorization decorator
def role_required(roles):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if not hasattr(g, 'user'):
return jsonify({'error': 'Authentication required'}), 401
if g.user['role'] not in roles:
return jsonify({'error': 'Insufficient permissions'}), 403
return f(*args, **kwargs)
return decorated
return decorator
# Validate file type using magic bytes
def validate_file_type(file_path):
try:
mime = magic.Magic(mime=True)
detected_type = mime.from_file(file_path)
if detected_type not in ALLOWED_FILE_TYPES:
return {'valid': False, 'type': None}
return {'valid': True, 'type': detected_type}
except Exception as e:
print(f"File type validation error: {e}")
return {'valid': False, 'type': None}
# Validate image content
def validate_image_content(file_path, mime_type):
try:
if mime_type.startswith('image/'):
with Image.open(file_path) as img:
# Check image dimensions
width, height = img.size
if width > 8000 or height > 8000:
return {'valid': False, 'reason': 'Image dimensions too large'}
# Check file size
if os.path.getsize(file_path) > MAX_FILE_SIZE:
return {'valid': False, 'reason': 'File too large'}
# Additional checks could be added here
return {'valid': True}
return {'valid': True}
except Exception as e:
return {'valid': False, 'reason': f'Invalid image format: {str(e)}'}
# File upload endpoint
@app.route('/api/upload', methods=['POST'])
@token_required
def upload_file():
try:
# Check if file part exists
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400
file = request.files['file']
# Check if file is selected
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
# Check file size
file.seek(0, os.SEEK_END)
file_size = file.tell()
file.seek(0)
if file_size > MAX_FILE_SIZE:
return jsonify({'error': f'File too large (max: {MAX_FILE_SIZE // 1024 // 1024}MB)'}), 400
# Save file to temporary location
temp_filename = str(uuid.uuid4())
temp_path = os.path.join(TEMP_UPLOAD_DIR, temp_filename)
file.save(temp_path)
try:
# Validate file type
type_validation = validate_file_type(temp_path)
if not type_validation['valid']:
os.unlink(temp_path)
return jsonify({'error': 'Invalid file type'}), 400
# Validate file content
content_validation = validate_image_content(temp_path, type_validation['type'])
if not content_validation['valid']:
os.unlink(temp_path)
return jsonify({'error': 'Invalid file content', 'reason': content_validation['reason']}), 400
# Create user directory if it doesn't exist
user_dir = os.path.join(PRIVATE_STORAGE_DIR, str(g.user['id']))
os.makedirs(user_dir, exist_ok=True)
# Generate secure filename
file_id = str(uuid.uuid4())
extension = ALLOWED_FILE_TYPES[type_validation['type']]
secure_filename = f"{file_id}.{extension}"
storage_path = os.path.join(user_dir, secure_filename)
# Move file to secure storage
shutil.move(temp_path, storage_path)
# Store file metadata in database
db = get_db()
db.execute(
'''
INSERT INTO files
(id, user_id, original_name, storage_path, mime_type, size, uploaded_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
''',
(
file_id,
g.user['id'],
file.filename,
os.path.relpath(storage_path, PRIVATE_STORAGE_DIR),
type_validation['type'],
file_size,
datetime.datetime.now().isoformat()
)
)
db.commit()
return jsonify({
'message': 'File uploaded successfully',
'fileId': file_id
}), 200
except Exception as e:
# Clean up temporary file if it still exists
if os.path.exists(temp_path):
os.unlink(temp_path)
raise e
except Exception as e:
print(f"Upload error: {e}")
return jsonify({'error': 'File upload failed'}), 500
# Get file metadata
@app.route('/api/files/<file_id>', methods=['GET'])
@token_required
def get_file_metadata(file_id):
try:
db = get_db()
# Get file metadata
file = db.execute(
'SELECT * FROM files WHERE id = ?',
(file_id,)
).fetchone()
if not file:
return jsonify({'error': 'File not found'}), 404
# Check if user has access to this file
if file['user_id'] != g.user['id'] and g.user['role'] != 'admin':
return jsonify({'error': 'Access denied'}), 403
# Return file metadata (without storage path)
return jsonify({
'id': file['id'],
'originalName': file['original_name'],
'mimeType': file['mime_type'],
'size': file['size'],
'uploadedAt': file['uploaded_at']
}), 200
except Exception as e:
print(f"File metadata error: {e}")
return jsonify({'error': 'Failed to get file metadata'}), 500
# Serve file content
@app.route('/api/files/<file_id>/content', methods=['GET'])
@token_required
def get_file_content(file_id):
try:
db = get_db()
# Get file metadata
file = db.execute(
'SELECT * FROM files WHERE id = ?',
(file_id,)
).fetchone()
if not file:
return jsonify({'error': 'File not found'}), 404
# Check if user has access to this file
if file['user_id'] != g.user['id'] and g.user['role'] != 'admin':
return jsonify({'error': 'Access denied'}), 403
# Construct full file path
file_path = os.path.join(PRIVATE_STORAGE_DIR, file['storage_path'])
# Check if file exists
if not os.path.exists(file_path):
return jsonify({'error': 'File content not found'}), 404
# Serve file with appropriate headers
return send_file(
file_path,
mimetype=file['mime_type'],
as_attachment=False,
download_name=file['original_name'],
etag=True,
last_modified=datetime.datetime.fromisoformat(file['uploaded_at']).timestamp(),
max_age=300 # 5 minutes cache
)
except Exception as e:
print(f"File serving error: {e}")
return jsonify({'error': 'Failed to serve file'}), 500
# Delete file
@app.route('/api/files/<file_id>', methods=['DELETE'])
@token_required
def delete_file(file_id):
try:
db = get_db()
# Get file metadata
file = db.execute(
'SELECT * FROM files WHERE id = ?',
(file_id,)
).fetchone()
if not file:
return jsonify({'error': 'File not found'}), 404
# Check if user has access to this file
if file['user_id'] != g.user['id'] and g.user['role'] != 'admin':
return jsonify({'error': 'Access denied'}), 403
# Construct full file path
file_path = os.path.join(PRIVATE_STORAGE_DIR, file['storage_path'])
# Delete file from storage
if os.path.exists(file_path):
os.unlink(file_path)
# Delete file record from database
db.execute('DELETE FROM files WHERE id = ?', (file_id,))
db.commit()
return jsonify({'message': 'File deleted successfully'}), 200
except Exception as e:
print(f"File deletion error: {e}")
return jsonify({'error': 'Failed to delete file'}), 500
# Schema for database initialization
@app.route('/api/init-db', methods=['POST'])
def initialize_database():
try:
init_db()
return jsonify({'message': 'Database initialized successfully'}), 200
except Exception as e:
print(f"Database initialization error: {e}")
return jsonify({'error': 'Failed to initialize database'}), 500
if __name__ == '__main__':
# In production, use a proper WSGI server and set debug=False
app.run(host='0.0.0.0', port=int(os.environ.get('PORT', 8080)), debug=False)
This implementation:
- Uses JWT authentication
- Limits file size to 5MB
- Validates file types using magic bytes
- Performs additional content validation for images
- Generates secure filenames with UUID
- Stores files in a private directory with user-specific paths
- Associates files with users in the database
- Implements proper authorization for file access
- Sets appropriate headers when serving files
- Provides endpoints for file metadata, content, and deletion
Best Practices for File Upload Security
To ensure your vibe-coded file upload functionality remains secure, follow these best practices:
1. Use Specific Prompts for Security
When using AI tools to generate file upload code, include security requirements in your prompts:
Instead of:
Create a file upload feature
Use:
Create a secure file upload feature with proper authentication, file type validation, content validation, secure storage, and access controls. Ensure files are stored in a private location, validate file types using content inspection, generate secure filenames, and implement proper authorization for file access.
2. Implement Defense in Depth
Don’t rely on a single security measure:
- Combine authentication, authorization, and input validation
- Validate both file type and content
- Store files securely outside the web root
- Implement proper access controls
- Use secure file serving mechanisms
3. Validate File Types Properly
Go beyond MIME type checking:
- Use file signature (magic bytes) detection
- Don’t trust the Content-Type header
- Implement whitelist-based validation
- Perform format-specific validation
- Reject files that don’t match expected formats
4. Implement Secure File Storage
Store files securely:
- Use a private directory outside the web root
- Implement user-specific subdirectories
- Generate secure random filenames
- Don’t include original filenames in storage paths
- Set appropriate file permissions
5. Control File Access
Implement proper access controls:
- Authenticate users before allowing file access
- Verify that users have permission to access specific files
- Use indirect references (file IDs) instead of direct paths
- Generate short-lived signed URLs for cloud storage
- Set appropriate headers when serving files
6. Validate File Content
Perform content-specific validation:
- Validate image dimensions and format
- Check for malicious content in PDFs
- Scan for viruses or malware when possible
- Reject files with suspicious characteristics
- Implement format-specific validation
7. Limit File Size and Quantity
Prevent resource exhaustion:
- Set appropriate file size limits
- Limit the number of files per request
- Implement user-specific storage quotas
- Clean up temporary files
- Monitor storage usage
8. Implement Secure File Processing
Process files securely:
- Avoid using command-line tools with user-controlled input
- Use secure libraries for file processing
- Validate files before processing
- Handle errors gracefully
- Clean up temporary files
Conclusion
File upload functionality is a critical component of many web applications, but it also introduces significant security risks if not implemented correctly. In the context of vibe coding, where AI tools might prioritize functionality over security, it’s essential to understand common file upload vulnerabilities and implement secure alternatives.
The key principles for securing file uploads in vibe-coded applications include:
- Validating file types properly using file signatures instead of relying on MIME types
- Implementing secure filename handling to prevent path traversal and other attacks
- Setting appropriate file size restrictions to prevent denial of service attacks
- Storing files securely in private locations with proper access controls
- Validating file content to reject malicious or suspicious files
- Implementing proper authentication and authorization for file access
- Using secure file serving mechanisms with appropriate headers and access controls
Each full-stack app builder—Lovable.dev, Bolt.new, Tempo Labs, Base44, and Replit—has its own file handling implementation patterns and potential vulnerabilities. By applying platform-specific security enhancements and following best practices, you can ensure that your vibe-coded applications maintain robust protection against file-related threats while still benefiting from the rapid development that AI tools enable.
Remember that a single file upload vulnerability can compromise your entire application. Investing time in securing your file handling functionality will protect your users’ data and your reputation from the devastating consequences of a successful attack.
Additional Resources
- OWASP File Upload Cheat Sheet
- SANS: Secure File Upload in Web Applications
- Mozilla Web Security: File Upload
- Content-Security-Policy for File Uploads
- NIST: Guide to General Server Security
References
- Web Application Security Consortium. (2023). “State of Web Application Security Report.” Retrieved from https://webappsec-consortium.org/reports
- OWASP. (2023). “File Upload Vulnerabilities.” Retrieved from https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload
- SANS Institute. (2022). “Secure Coding Practices: File Upload Handling.” Retrieved from https://www.sans.org/blog/secure-file-upload-in-web-applications/
- Mozilla. (2023). “Web Security Guidelines: File Upload.” Retrieved from https://infosec.mozilla.org/guidelines/web_security#file-upload
- National Institute of Standards and Technology. (2021). “Guide to General Server Security.” Retrieved from https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-123.pdf