DAST scanner

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.

DASTHigh–CriticalInjection Riskfile_upload_scanner

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.

Endpoint inventory POST /api/upload/upload POST /profile/avatar/avatar GET /files/list/file GET /api/usersno word POST /api/ordersno word path only, lowercased, any method Eight path indicators /upload/file/media/attachment /image/avatar/document/import Tested endpoints /api/upload/profile/avatar/files/list GET /files/list is kept too: the match is a substring, so /file hits /files/list. No match anywhere? The scanner sends nothing and returns no findings.

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.

Request POST /api/upload Content-Type: multipart/form-data part name: "file" filename="…"part type: … body: … session cookie or bearer added if one exists filename="test.svg"part type: image/svg+xml body: <svg onload="alert(1)"> filename="../../../etc/passwd"part type: text/plain body: test Response 201 Created 2xx + non-failure body + storage evidence → accepted 415 Unsupported Media Type outside 200 to 299 → not accepted The body is read; a same-host URL may be fetched back. A network error counts as not accepted. Two type probes, one finding each if accepted: test.html text/html <script>alert(1)</script> test.svg image/svg+xml <svg onload="alert(1)"> Three traversal probes, then the first accepted one ends the endpoint: ../../../etc/passwd ..\..\..\etc\passwd ....//....//etc/passwd File upload accepts dangerous file types High · confidence 0.85 · one finding for each of .html and .svg that is accepted File upload vulnerable to path traversal Critical · confidence 0.90 · the first accepted name is reported, the rest are skipped No finding The server refused the file. That is the expected behaviour of a protected endpoint.

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.

400 Bad Request {"error":"bad type"} status outside 2xx no finding the server refused the file 201 Created {"url":"/u/test.svg"} status inside 2xx High finding a real accept; the URL is fetched back and serves the file 200 OK {"error":"bad type"} status inside 2xx no finding failure body skipped: the body refuses, though the status does not 400 Bad Request missing field "avatar" the part is always named file silent miss an endpoint that expects another field name is never really tested

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

Impact

Unrestricted file uploads let an attacker:

  • Execute code on your server. Uploading .php, .jsp or .sh files that the server executes.
  • Plant stored XSS. An .html or .svg file with JavaScript runs when someone opens it on your domain.
  • Overwrite system files. A traversal name such as ../../etc/cron.d/backdoor can write anywhere the process can.
  • Serve malware. Your domain becomes a malware distribution point.

Real-world breach

ImageTragick · 2016

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.

filename = photo.png test.svg ../../../etc/passwd 1 · Extension allowlist .jpg .jpeg .png .webp path.extname(file.name) 2 · Content-type allowlist image/jpeg · png · webp file.type 3 · Random filename crypto.randomUUID() + ext the client's name is discarded Extension .png and type image/png are on the lists → saved as 3f9c…e1.png. .svg is not on the extension list → 400 "File type not allowed". The type probe is refused. Extension is empty → 400. Even if it passed, the saved name is a UUID, so no path survives.

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}` })
}

Limits and what it deliberately stays quiet about

AspectBehaviour
Endpoint selectionPath substring only, from eight indicators. Methods and forms are ignored.
Types testedOnly .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 mismatchNot tested. Each file is posted with a content type that matches its extension.
ProofA 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 fieldAlways file.
SafetyBodies 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.
TimeEndpoints are probed in order and the scanner stops cooperatively before its hard timeout, returning the findings gathered so far.