# File Upload Security Testing (DAST)

Escape discovers file upload endpoints from the signals collected during a scan 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). The module runs automatically inside your existing DAST scan with no separate profile or infrastructure.

The module hooks into the DAST scan types you already run:

| Surface | Scanner kind | What we look at |
| --- | --- | --- |
| WebApp DAST | Web application | Live multipart traffic captured by the agentic crawler + JS |
| REST API DAST | REST API | Recorded API exchanges with `Content-Disposition: filename=` |
| GraphQL API DAST | GraphQL API | 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 hasn't exercised yet: `FormData`, `<input type="file">`, imports of `apollo-upload-client`, `react-dropzone`, `tus-js-client`, `@uppy/core`, `filepond`, and `fineuploader`. JS-only endpoints appear as informational file upload findings and aren't actively probed.

When endpoint signals are ambiguous, Escape uses an AI-assisted classifier to decide whether the surface is likely to accept uploads. Confirmed upload endpoints are summarized in the inventory so you can review their role even when no vulnerability is found.

### Customer Safety

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

- Test uploads use distinct filenames to reduce the risk of overwriting existing 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.
- Default XXE and SSRF checks use OOB callbacks rather than attempts to read local files.
- 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. See [Out-of-Band Testing](<https://docs.escape.tech/documentation/dast/out-of-band-testing/index.md>) for OOB collector behaviour shared with the LLM module's SSRF check.

### Evidence

When a file upload issue is raised, the finding explains which endpoint was tested, which check confirmed the behavior, and the non-destructive canary evidence used for confirmation.

## Enabled by Default

The module runs automatically as part of your DAST scan and requires no configuration. All seven active checks are enabled by default.

To disable a check, set its `skip` field:

```yaml
security_tests:
  file_upload_ssrf:
    skip: true
```

Supported keys are `file_upload_detected`, `file_upload_unrestricted`, `file_upload_stored_xss`, `file_upload_rce`, `file_upload_path_traversal`, `file_upload_xxe`, `file_upload_ssrf` and `file_upload_zip_slip`.
