

0 / 2 embers
0 / 3000 xp
click for more info
Complete a lesson to start your streak
click for more info
Still calibrating
click for more info
Not enough gems
Cost: 6 gems
1: Injection
incomplete
2: Fixing SQL Injection
incomplete
3: Injection Beyond SQL
incomplete
4: Safe Validation and Sanitization
incomplete
5: When to Sanitize
incomplete
6: Unsafe Archive Extraction
incomplete
7: Safe Archive Extraction
incomplete
8: LLM Prompt Injection
incomplete
9: Limiting Tool Calls
incomplete
10: Narrow Tool Interfaces
incomplete
11: File Upload Security
incomplete
Back
ctrl+,
Next
ctrl+.
This lesson's interactive features are locked, please to keep using them
File uploads are one of the highest-risk features in web applications. They accept large amounts of user-controlled data in complex formats that may later be executed, parsed, or served. Attackers love to target upload handlers aggressively.
An unsafe handler trusts what the client tells it:
const upload = multer({ storage: multer.memoryStorage() });
app.post(
"/upload",
upload.single("file"),
async (req: Request, res: Response) => {
const file = req.file;
if (!file) return res.status(400).json({ error: "No file" });
await writeFile(`./public/uploads/${file.originalname}`, file.buffer);
res.status(201).json({ message: "Uploaded" });
},
);
It writes to an attacker-controlled filename and stores the uninspected contents into a public directory. That means an attacker can upload a dangerous shell.js script renamed to image.png and place it somewhere the application or a browser may actually execute it.
You might think file.mimetype gives you the real content type, but it does not – libraries like multer simply read mimetype from the Content-Type header, which is trivially spoofable by the client.
A safer handler generates its own filename, inspects the file's actual bytes, and stores the file outside any public paths:
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 2 * 1024 * 1024 },
});
const ALLOWED_TYPES = new Set(["image/png", "image/jpeg"]);
app.post(
"/upload",
upload.single("file"),
async (req: Request, res: Response) => {
const file = req.file;
if (!file) return res.status(400).json({ error: "No file" });
const detected = await fileTypeFromBuffer(file.buffer);
if (!detected || !ALLOWED_TYPES.has(detected.mime)) {
return res.status(400).json({ error: "Invalid file type" });
}
const safeName = `${randomUUID()}.${detected.ext}`;
await writeFile(`./private/uploads/${safeName}`, file.buffer);
res.status(201).json({ message: "Uploaded", id: safeName });
},
);
With file uploads, you should enforce:
See OWASP's full File Upload Cheat Sheet for a full defense-in-depth approach.
Bearly Secure already receives tax-document bytes in memory and persists their metadata, but storeTaxDocument accepts any bytes and gives every file a generic name. Detect each file's type before persistence and generate a safe storage name.
With Bearly Secure still running, run and submit the CLI tests from the project root.