File Upload
An upload endpoint that accepts anything lets an attacker plant a script on your domain or write outside the upload folder. The scanner finds endpoints whose path looks like an upload, posts an HTML file, an SVG file and three traversal filenames as multipart form data, and judges each by a 2xx with a non-failure body plus evidence that the file was stored.
Finding upload endpoints
The scanner does not look at forms or methods. It reads the path of every endpoint in the inventory and keeps the ones that contain an upload word.
1The scanner starts from the endpoint inventory built by discovery and the crawl: method and URL for every endpoint.
2An endpoint is kept if its lowercased path contains one of eight words: /upload, /file, /media, /attachment, /image, /avatar, /document, /import. Method, form fields and content type are not consulted.
3The kept list is what gets probed. Because the match is a substring, a read-only GET /files/list is included, and an upload sink on a path like /api/orders is never tested.
The five probes
For every kept endpoint the scanner posts five small files, one at a time. Two test which file types the server accepts. Three test whether it accepts a filename that climbs out of the upload folder.
1Each probe is a multipart POST with a single part named file, carrying a filename, a part content type and a tiny body. The session's auth headers are attached when the scan has a session.
2Type probes. test.html (text/html) and test.svg (image/svg+xml), each with a script payload. A 2xx for either is a High finding titled "File upload accepts dangerous file types", because an uploaded HTML or SVG file served back runs script on your origin.
3Traversal probes. A harmless test body under three filenames in order. The first accepted one (2xx that echoes the unsanitised name) is a Critical finding, "File upload vulnerable to path traversal", and the remaining names are skipped for that endpoint.
4A 2xx alone is not enough. The body must not be a failure shape (such as success: false or an error field), and there must be evidence of storage: a same-host URL from the response that serves the uploaded file back (or whose path ends in the uploaded extension), or the response referencing the stored name. If a type probe gets a clean 2xx but none of that evidence, it is still reported, at Medium and confidence 0.4, saying evidence of storage was not found. For traversal probes the response must echo the unsanitised filename. A 4xx, a 5xx or a failed connection is never accepted. A server that stores only the basename is not reported for traversal.
What acceptance does and does not prove
A 2xx alone is not enough: the body must not be a failure shape and there must be evidence the file was stored. The finding still does not prove the file is served as active content. The four replies below show where the line is.
1A refusing server answers 4xx. No finding, no noise.
2A server that stores the file answers 2xx, usually with a URL or the stored name. The scanner opens a same-host URL to see if the payload is served back, and for traversal checks that the raw name was echoed.
3Some APIs return 200 and put the refusal in the JSON body. The scanner reads the body and skips these failure shapes. A bare {"success": true} that never mentions the file is also not reported.
4The part is always named file. An endpoint that requires a different field name rejects every probe and is silently skipped.
Why it matters
Unrestricted file uploads let an attacker:
- Execute code on your server. Uploading
.php,.jspor.shfiles that the server executes. - Plant stored XSS. An
.htmlor.svgfile with JavaScript runs when someone opens it on your domain. - Overwrite system files. A traversal name such as
../../etc/cron.d/backdoorcan write anywhere the process can. - Serve malware. Your domain becomes a malware distribution point.
Real-world breach
CVE-2016-3714 in ImageMagick allowed remote code execution via crafted image files. Any service that accepted user-uploaded images and processed them with ImageMagick was vulnerable.
The fix: three gates before the file is saved
Check the extension, check the declared type, and never use the client's filename. Watch three uploads go through.
1The happy path. The file is checked on extension and declared type, then stored under a name the server chose.
2Gate one: extension allowlist. .svg, .html and the other dangerous types are not on it, so the scanner's type probes get a 400.
3Gate three removes traversal at the root. A random UUID name means the filename the client sent never touches the filesystem, so ../ has nothing to climb with.
Vulnerable vs fixed
// BAD: accepts any file and keeps the client's filename
export async function POST(request: Request) {
const file = (await request.formData()).get("file") as File
await fs.writeFile(path.join(UPLOAD_DIR, file.name), Buffer.from(await file.arrayBuffer()))
return NextResponse.json({ url: `/uploads/${file.name}` })
}// GOOD: Whitelist allowed extensions and validate content
const ALLOWED_TYPES = ["image/jpeg", "image/png", "image/webp"]
const ALLOWED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".webp"]
export async function POST(request: Request) {
const file = formData.get("file") as File
const ext = path.extname(file.name).toLowerCase()
if (!ALLOWED_EXTENSIONS.includes(ext)) {
return NextResponse.json({ error: "File type not allowed" }, { status: 400 })
}
if (!ALLOWED_TYPES.includes(file.type)) {
return NextResponse.json({ error: "Invalid content type" }, { status: 400 })
}
// Generate a random filename (prevents path traversal)
const safeFilename = `${crypto.randomUUID()}${ext}`
// ... save with safeFilename
}Limits and what it deliberately stays quiet about
| Aspect | Behaviour |
|---|---|
| Endpoint selection | Path substring only, from eight indicators. Methods and forms are ignored. |
| Types tested | Only .html and .svg. Server-executable types such as .php, .jsp and .sh are not probed. |
| Traversal names | ../../../etc/passwd, ..\..\..\etc\passwd, ....//....//etc/passwd. One finding per endpoint at most. |
| Content-type mismatch | Not tested. Each file is posted with a content type that matches its extension. |
| Proof | A 2xx status, a body that is not a failure shape, and storage evidence: a same-host URL from the response that serves the payload back or ends in the uploaded extension, or the response referencing the stored name. Without storage evidence a type probe is reported only as Medium at confidence 0.4. Traversal probes need the raw filename echoed. The scanner does not check where a traversal name actually landed on disk. |
| Form field | Always file. |
| Safety | Bodies are a few bytes and scripts only alert. The scanner also issues follow-up GETs to same-host URLs named in the reply (never URLs with a query string or a path containing logout, signout, delete, remove, destroy or reset). Accepted files stay on the target, so remove test files such as test.html and test.svg if the server stored them. |
| Time | Endpoints are probed in order and the scanner stops cooperatively before its hard timeout, returning the findings gathered so far. |