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 headerContent-Disposition: form-data; name=...; filename="...".- GraphQL multipart spec:
operations+map+ numbered file parts (the standardapollo-upload-clientshape). - Presigned cloud upload:
POSTorPUTagainst a recognised S3 / GCS / R2 / Azure host carryingX-Amz-Signature=,X-Goog-Signature=, orsig=. - Two-step presigned negotiation: a JSON response that contains
uploadUrl,presignedUrl,signedUrl,upload_url, orurl. - Resumable
tusprotocol:Tus-Resumable/Upload-Lengthheaders. - 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.mdfor 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:
When the flag is off (default) every task short-circuits at the top and the module produces no events.