Skip to content

File Upload Security Testing (DAST)

Escape discovers every file upload endpoint your application exposes and probes each one against the OWASP Top 10 categories that matter for upload surfaces (A01:2021 access control, A03:2021 injection, A04:2021 insecure design, A05:2021 misconfiguration, A10:2021 SSRF). Detection is opt-in via a single experimental flag; the rest of the module runs inside your existing DAST scan with no separate profile or infrastructure.

The module hooks into the three DAST scan kinds you already run:

Surface Scanner kind What we look at
WebApp DAST FRONTEND_DAST Live multipart traffic captured by the agentic crawler + JS
REST API DAST BLST_REST Recorded API exchanges with Content-Disposition: filename=
GraphQL API DAST BLST_GRAPHQL Same recorded exchanges, GraphQL multipart spec aware

Coverage

Seven active checks plus an informational ISSUE_FILE_UPLOAD_DETECTED per detected endpoint. Every check confirms on a deterministic signal that customers can audit from the event log.

Check Issue Severity Compliance How we confirm it
Unrestricted Upload ISSUE_FILE_UPLOAD_UNRESTRICTED High CWE-434 + OWASP A04:2021 Canary file uploaded under a dangerous extension; confirms only on 2xx upload AND canary readable through the inferred URL.
Stored XSS via Upload ISSUE_FILE_UPLOAD_STORED_XSS High CWE-79 + OWASP A03:2021 SVG / HTML / PDF / polyglot canaries; confirms only when retrieval Content-Type is image/svg+xml or text/html AND canary is reflected verbatim.
RCE Polyglot ISSUE_FILE_UPLOAD_RCE High CWE-94 + CWE-434 + A03 Idempotent GIF+PHP / .htaccess / web.config / .user.ini / ZIP polyglot; confirms via canary token echo or OOB callback at ssrf.tools.escape.tech.
Path Traversal via Upload ISSUE_FILE_UPLOAD_PATH_TRAVERSAL High CWE-22 + OWASP A01:2021 Canary-suffixed traversal filenames (../escape_canary_<uuid>.txt, null-byte, Windows backslash, RTLO); confirms only on canary read-back.
XXE via Upload ISSUE_FILE_UPLOAD_XXE High CWE-611 + OWASP A05:2021 SVG / OOXML / xlsx-style XML with OOB-only external entities; confirms via tagged callback at the OOB collector.
SSRF via Upload ISSUE_FILE_UPLOAD_SSRF High CWE-918 + OWASP A10:2021 SVG xlink:href / <image href> / XML SYSTEM references to the OOB collector; confirms via tagged callback with fu- prefix.
Zip Slip ISSUE_FILE_UPLOAD_ZIP_SLIP High CWE-22 Archive entries with canary-suffixed traversal paths; ZIP bomb capped at 5x ratio and 1 MiB on the wire; confirms only on canary read-back.

How tests are executed

flowchart LR
    HAR[Live traffic / recorded exchanges / JS source] --> DET[Endpoint discovery]
    DET --> Reduce[Endpoint consolidation]
    Reduce --> Profile[Endpoint profiling]
    Profile --> Checks[7 active checks in parallel]
    Checks --> Issues[(SecurityIssues)]

Detection methodology

Escape's detection layer reads signals already collected during your DAST scan and emits one FileUploadEndpointCandidate per upload-shaped exchange. The reducer dedupes them by (transport, url, GraphQL operation, method), profiles each replayable endpoint, and emits ISSUE_FILE_UPLOAD_DETECTED so you can see the surface even when no active check confirms.

Network-layer signals (any one is enough to short-list a candidate):

  • Content-Type: multipart/form-data; boundary=... with at least one part header Content-Disposition: form-data; name=...; filename="...".
  • GraphQL multipart spec: operations + map + numbered file parts (the standard apollo-upload-client shape).
  • Presigned cloud upload: POST or PUT against a recognised S3 / GCS / R2 / Azure host carrying X-Amz-Signature=, X-Goog-Signature=, or sig=.
  • Two-step presigned negotiation: a JSON response that contains uploadUrl, presignedUrl, signedUrl, upload_url, or url.
  • Resumable tus protocol: Tus-Resumable / Upload-Length headers.
  • AWS S3 multipart: POST ?uploads, PUT ?partNumber=.

Path heuristics corroborate the above: /upload, /files, /attachment, /media, /import, /avatar, /image, /photo, /document, /asset, /picture, /profile/picture, /api/Complaints. A path keyword on its own is never enough; a multipart, presigned, or two-step signal must also be present.

JavaScript-source signals catch endpoints the crawler has not exercised yet: FormData, <input type="file">, imports of apollo-upload-client, react-dropzone, tus-js-client, @uppy/core, filepond, and fineuploader. JS-only endpoints produce ISSUE_FILE_UPLOAD_DETECTED from deterministic evidence and are not actively probed.

A tightly budgeted tiny-LLM judge (ModelName.gpt_5_nano) breaks ties on ambiguous endpoints, capped at five calls per scan. The profile step uses the larger ModelName.gpt_5_mini to summarise upload semantics for the inventory description.

Customer safety

The default catalogues are designed to surface the vulnerability without polluting your environment:

  • Every payload filename carries a per-scan escape_canary_<uuid> suffix, so resolved paths cannot collide with real customer files.
  • Path traversal and zip slip variants assert on canary read-back only. We never write to /etc, ~/.ssh, cron directories, or any real-looking filename.
  • ZIP bomb canaries are capped at 5x compression ratio and 1 MiB on the wire.
  • XXE and SSRF default catalogues are OOB-only. Local file exfiltration (file:///etc/passwd, ...) is intentionally out of scope in the first release.
  • RCE polyglot canaries are idempotent echoes of the canary token. No shells, no filesystem writes, no privileged operations.
  • Uploaded artefacts remain in your storage until your normal retention sweeps. Cross-link to 35-out-of-band-testing.md for OOB collector behaviour shared with the LLM module's SSRF check.

Sample event log

Every probe emits one context.info event with the request and response excerpts as attachments, grouped under file-upload:{cwe}:{check}:{url}:{filename} so you can drill down from the issue to the exact traffic Escape sent.

title: "file-upload-rce-polyglot: gif-php-echo"
stage: active-checks
event_group_id: "file-upload:CWE-94:file-upload-rce-polyglot:https://app.example.com/api/Complaints/file:escape_canary_a3f2c1.php"
attachments:
  - name: request
    excerpt: |
      POST /api/Complaints/file HTTP/1.1
      Content-Type: multipart/form-data; boundary=----xxx
      ...
      ------xxx
      Content-Disposition: form-data; name="file"; filename="escape_canary_a3f2c1.php"
      Content-Type: image/gif

      GIF89a
      <?php echo "escape-canary a3f2c1"; ?>
      ------xxx--
  - name: response
    excerpt: |
      HTTP/1.1 201 Created
      Content-Type: application/json

      { "url": "https://app.example.com/files/escape_canary_a3f2c1.php" }

Opt-in

The module is gated on a single experimental flag. Add it to your scan configuration and re-run the DAST scan:

experimental:
  file_upload_security_testing: true

When the flag is off (default) every task short-circuits at the top and the module produces no events.