Harness-engineering owasp-file-upload-security

File Upload Security

install
source · Clone the upstream repo
git clone https://github.com/Intense-Visions/harness-engineering
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/codex/owasp-file-upload-security" ~/.claude/skills/intense-visions-harness-engineering-owasp-file-upload-security-440645 && rm -rf "$T"
manifest: agents/skills/codex/owasp-file-upload-security/SKILL.md
source content

File Upload Security

Secure file upload endpoints against malicious files, path traversal, resource exhaustion, and code execution

When to Use

  • Building file upload endpoints (avatars, documents, attachments)
  • Accepting user-generated content that includes files
  • Storing uploaded files on disk or cloud storage
  • Serving uploaded files back to users
  • Reviewing existing upload functionality for security gaps

Instructions

  1. Validate file type by magic bytes, not just the extension or Content-Type header. Users can rename
    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;
}
  1. Enforce file size limits. Prevent resource exhaustion by limiting both individual file size and total request size.
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
});
  1. Generate random filenames. Never use the original filename — it can contain path traversal sequences (
    ../../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}`;
}
  1. Store uploads outside the web root. Never store uploaded files in a directory served by your web server. Use a dedicated storage directory or cloud storage (S3, GCS).
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
    })
  );
}
  1. Serve files with
    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);
});
  1. 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.

  2. 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;
}
  1. Strip metadata from images to remove EXIF data that may contain GPS coordinates, camera info, or embedded scripts.
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();
}
  1. Implement upload rate limiting per user to prevent abuse.
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);
  1. Log all upload activity with user ID, file hash, file type, and result (accepted/rejected) for audit trails.

Details

Attack vectors addressed:

  • Malicious file execution: Attacker uploads a PHP/JSP shell disguised as an image. Mitigated by storing outside web root and generating random filenames.
  • Path traversal: Filename contains
    ../../
    to write outside the upload directory. Mitigated by generating random filenames.
  • XSS via uploaded HTML/SVG: Attacker uploads an HTML file with JavaScript. Mitigated by
    Content-Disposition: attachment
    ,
    X-Content-Type-Options: nosniff
    , and serving from a separate domain.
  • Denial of service: Attacker uploads very large files or many files. Mitigated by size limits and rate limiting.
  • Zip bombs: A small zip file that expands to gigabytes. If accepting archives, check uncompressed size before extraction.

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:

  • Use pre-signed URLs for direct upload to S3/GCS (avoids your server handling large files)
  • Set bucket policies to deny public access by default
  • Enable server-side encryption
  • Set lifecycle policies to auto-delete orphaned uploads

Common mistakes:

  • Trusting the
    Content-Type
    header (user-controlled)
  • Using the original filename for storage
  • Serving uploads from the same origin as the application
  • Not limiting file size at the server level (relying only on client-side limits)
  • Storing uploads in a publicly accessible directory

Source

https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html

Process

  1. Read the instructions and examples in this document.
  2. Apply the patterns to your implementation, adapting to your specific context.
  3. Verify your implementation against the details and edge cases listed above.

Harness Integration

  • Type: knowledge — this skill is a reference document, not a procedural workflow.
  • No tools or state — consumed as context by other skills and agents.

Success Criteria

  • The patterns described in this document are applied correctly in the implementation.
  • Edge cases and anti-patterns listed in this document are avoided.