From harness-claude
Secures Node.js file upload endpoints against malicious files, path traversal, resource exhaustion, and code execution using magic byte validation, multer limits, random filenames, and S3 storage.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Secure file upload endpoints against malicious files, path traversal, resource exhaustion, and code execution
Audits file upload handlers for vulnerabilities like unrestricted uploads, path traversal, executable files, and size issues. Suggests fixes with extension allowlists, renaming, secure storage outside webroot, and MIME validation.
Handles secure file uploads to S3 and Cloudflare R2 with presigned URLs, multipart uploads, image optimization, and large file streaming. Addresses security pitfalls like magic byte validation and size limits.
Implements secure file upload handling with validation, virus scanning, storage management, and serving across Flask, Express, FastAPI. Use for file upload features, document management, and media storage.
Share bugs, ideas, or general feedback.
Secure file upload endpoints against malicious files, path traversal, resource exhaustion, and code execution
malware.exe to photo.jpg and set any Content-Type. Inspect the file's magic bytes (first few bytes of the file content).import { fileTypeFromBuffer } from 'file-type';
const ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'application/pdf']);
async function validateFileType(buffer: Buffer): Promise<string> {
const type = await fileTypeFromBuffer(buffer);
if (!type || !ALLOWED_TYPES.has(type.mime)) {
throw new ValidationError(`File type not allowed: ${type?.mime ?? 'unknown'}`);
}
return type.mime;
}
import multer from 'multer';
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB per file
files: 5, // Max 5 files per request
fieldSize: 1024, // Max 1 KB for text fields
},
storage: multer.memoryStorage(), // Store in memory for validation before saving
});
// Apply middleware
app.post('/upload', upload.single('file'), async (req, res) => {
if (!req.file) return res.status(400).json({ error: 'No file provided' });
const mime = await validateFileType(req.file.buffer);
// ... save file
});
../../etc/passwd) or special characters that break storage systems.import crypto from 'node:crypto';
import path from 'node:path';
function generateSafeFilename(originalName: string): string {
const ext = path.extname(originalName).toLowerCase();
const allowedExtensions = new Set(['.jpg', '.jpeg', '.png', '.webp', '.pdf']);
if (!allowedExtensions.has(ext)) {
throw new ValidationError(`Extension not allowed: ${ext}`);
}
return `${crypto.randomUUID()}${ext}`;
}
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
async function uploadToS3(buffer: Buffer, filename: string, contentType: string) {
const client = new S3Client({ region: 'us-east-1' });
await client.send(
new PutObjectCommand({
Bucket: process.env.UPLOAD_BUCKET,
Key: `uploads/${filename}`,
Body: buffer,
ContentType: contentType,
ContentDisposition: 'attachment', // Force download, prevent inline execution
})
);
}
Content-Disposition: attachment to prevent browsers from executing uploaded content (e.g., HTML files with JavaScript).app.get('/files/:id', async (req, res) => {
const file = await getFileMetadata(req.params.id);
res.setHeader('Content-Type', file.mimeType);
res.setHeader('Content-Disposition', `attachment; filename="${file.displayName}"`);
res.setHeader('X-Content-Type-Options', 'nosniff');
const stream = await getFileStream(file.storagePath);
stream.pipe(res);
});
Serve uploaded files from a separate domain. Use a different origin (e.g., uploads.example.com) so that any XSS in uploaded files cannot access cookies or APIs on your main domain.
Scan uploads for malware in high-security environments. Use ClamAV or a cloud-based scanning service.
import NodeClam from 'clamscan';
const clam = await new NodeClam().init({
clamdscan: { socket: '/var/run/clamav/clamd.ctl' },
});
async function scanFile(filePath: string): Promise<boolean> {
const { isInfected } = await clam.isInfected(filePath);
return !isInfected;
}
import sharp from 'sharp';
async function sanitizeImage(buffer: Buffer): Promise<Buffer> {
return sharp(buffer)
.rotate() // Auto-rotate based on EXIF, then strip EXIF
.withMetadata(false) // Remove all metadata
.toBuffer();
}
import rateLimit from 'express-rate-limit';
const uploadLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 20, // 20 uploads per window
message: { error: 'Too many uploads. Try again later.' },
keyGenerator: (req) => req.user?.id ?? req.ip,
});
app.post('/upload', uploadLimiter, upload.single('file'), handler);
Attack vectors addressed:
../../ to write outside the upload directory. Mitigated by generating random filenames.Content-Disposition: attachment, X-Content-Type-Options: nosniff, and serving from a separate domain.Image-specific risks: SVG files can contain JavaScript (<script> tags, onload handlers). If accepting SVG uploads, sanitize them by parsing and removing script elements, or convert to a raster format.
Cloud storage best practices:
Common mistakes:
Content-Type header (user-controlled)https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html