DEV Community

Davi
Davi

Posted on Originally published at blog.mago.team

REST API File Upload Attack Chains: MIME Bypass, Path Traversal, and SVG-to-XSS

A REST endpoint that accepts multipart/form-data looks unremarkable. A developer adds it in an afternoon: POST /api/upload, store the file, return the URL. The framework does not validate the body. The API docs say "image/png only." The attacker sends image/png in the Content-Type header and a PHP web shell as the body. It uploads. It executes.

The attack surface is not the file. It is the gap between what the server accepts, where it stores the result, and how it serves it back. Three CVEs in the CISA KEV catalog and four HackerOne reports show that this gap exists by default in REST APIs that skip explicit validation.

API File Upload Has No Browser and No Framework Safety Net

HTML form uploads pass through two automatic filters before reaching server code. The browser enforces MIME type selection before the request leaves the client. Web frameworks apply default content validation. REST API endpoints get neither.

Browsers restrict <input type="file"> to the accept attribute, filtering before the upload. API clients like curl, Postman, and fetch impose no such constraint. The OWASP File Upload Cheat Sheet is clear: never trust the MIME type of an uploaded file. Any HTTP client can spoof it.

Frameworks like Spring MVC, Express, and FastAPI parse the multipart boundary but do not validate Content-Type against the actual file content. Binary PUT endpoints receive a raw byte stream with no metadata the server can inspect without reading the entire body.

MIME Type Bypass: The Header Is a Suggestion

Server-side code that accepts or rejects uploads based on the Content-Type header can be bypassed by any HTTP client in one line. Three distinct bypass primitives exist: header spoofing, double extension, and null byte injection. Each exploits a different layer of the validation chain.

Header spoofing: a POST request with Content-Type: image/png containing <?php system($_GET['cmd']); ?> as the body. If the server reads request.headers['content-type'] to decide where to store the upload, the file is accepted and will be executable. Double extension bypass (.php.jpg): servers that check only the last file extension fail when some Apache configurations execute .php files regardless of additional extensions.

Null byte injection: filename=shell.php%00.jpg. In C-based extension checks (older PHP, CGI), the string terminates at the null byte. The extension check sees .jpg. The stored filename becomes shell.php. Magic byte spoofing: prepend FF D8 FF E0 (JPEG magic bytes) before the PHP payload. Both getimagesize() and mime_content_type() return valid. The PHP code stays intact after the header.

Stored XSS via SVG and HTML Upload

Any API that stores SVG or HTML and serves them from the application's own origin turns every upload endpoint into a stored XSS vector. SVG is XML that executes embedded <script> tags when rendered in a browser. The account-takeover chain requires no phishing. The attacker uploads, shares the file URL. The victim visits it on the application origin, and session cookies are exfiltrated.

HackerOne #212253 (Ubiquiti): attacker uploads SVG with XSS payload through an API endpoint. Serving the file URL delivers the script to any victim who visits it on the same origin. HackerOne #3008878 (LY Corporation / LINE): the chat.line.biz API endpoint accepted SVG uploads without filtering embedded JavaScript. Stored XSS with full DOM access. HackerOne #130591 (Moneybird): SVG accepted through the invoice attachment API. Payload executed in the accounting application context.

The browser's Same-Origin Policy is the attack enabler, not the defense. Content served from app.example.com/uploads/payload.svg runs with full access to cookies and localStorage on app.example.com. Setting Content-Disposition: attachment on served files forces download instead of inline rendering and closes this vector without requiring a separate domain.

Path Traversal via Filename

The filename field in a Content-Disposition header is attacker-controlled. Most upload handlers pass it directly to path.join() or an equivalent. An attacker who controls the stored filename controls where the file lands on disk. Targets include cron jobs, SSH authorized_keys, nginx configuration fragments, and application source files.

CVE-2022-29464 (WSO2 API Manager, CVSS 9.8, CISA KEV): unauthenticated POST to /fileupload with Content-Disposition: filename=../../../../repository/deployment/server/webapps/shell.jsp. The JSP shell lands in the webapps directory and executes. Exploited in the wild within 24 hours of disclosure. Rapid7 published a Metasploit module in the same period.

CVE-2024-53677 (Apache Struts2, CVSS 9.5, December 2024): the FileuploadInterceptor does not sanitize the upload path parameter. Path traversal sequences in the filename field place files outside the designated upload directory. The fix requires migration to the new Action File Upload mechanism, not a configuration change. A Jitsi API disclosure showed the /v/uploads endpoint passing file.originalname directly to path.join(RTCSTATS_DOWNLOADS_PATH, filename) without sanitization, enabling unauthenticated arbitrary file write.

Polyglot Files: When the Image Is Also the Exploit

A polyglot file is simultaneously valid in two formats. A file that passes PNG validation and executes as PHP when the extension changes does not bypass any validation. It passes it. The technique turns every server that processes uploaded images, including resize and EXIF strip operations, into a potential RCE surface.

JPEG-PHP polyglot: a valid JPEG header (FF D8 FF E0) followed by PHP code embedded in the EXIF Comment field. getimagesize() returns true. If the server stores the file with a .php extension or calls include() on it, the payload executes. PortSwigger Web Security Academy documents the polyglot web shell lab: upload a .php file renamed to .jpg with valid JPEG magic bytes. The server validates by magic bytes and stores the file. Changing the extension triggers execution.

ArXiv paper 2407.01529 systematically studies polyglot attack chains. PNG-PHP, PDF-HTML, and ZIP-JAR combinations each enable distinct execution paths depending on the server's processing context. CVE-2022-27925 (Zimbra, CVSS 7.2 chained to 9.8): the mboximport REST endpoint extracted ZIP archives without sanitizing paths. Mass exploitation followed in August 2022 after chaining with CVE-2022-37042 for authentication bypass. CISA KEV.

Three CVEs in CISA KEV, One Root Cause

CVE-2022-29464, CVE-2022-27925, and CVE-2024-53677 are all API file upload vulnerabilities that reached RCE. All three share the same structural failure: the storage path was derived from attacker-controlled input.

CVE-2022-29464 (WSO2 2.2.0 through 4.0.0): unauthenticated POST to /fileupload, path traversal via Content-Disposition, CVSS 9.8, CISA KEV, exploited within 24 hours. CVE-2022-27925 with CVE-2022-37042 (Zimbra 8.8.15 and 9.0): mboximport REST endpoint, ZIP without path sanitization. Chained with authentication bypass, it reached unauthenticated RCE. Mass campaign in August 2022. CVE-2024-53677 (Apache Struts2 through 6.3.0.2): FileuploadInterceptor, CVSS 9.5, affects all Struts2 versions since 2.0.0, fix requires mechanism migration.

The shared pattern: upload endpoint accepts filename from client input, passes it to the storage function without normalization. The file lands outside the intended directory.

Defenses That Actually Work

A complete defense requires five independent controls applied in sequence. Any single control is bypassable alone. The combination closes all three attack vectors: content type bypass, path traversal, and same-origin serving.

First: magic byte validation server-side, not by Content-Type header. Read the first 8 to 16 bytes of the file body. Libraries like python-magic, file-type (Node), or Apache Tika handle this without significant overhead. Second: server-generated filename. Generate a UUID for the storage key. Never use the client-supplied name for the actual storage path, even after sanitization. This closes path traversal entirely.

Third: separate origin for serving. Serve uploads from a cookieless CDN domain like uploads.example.com. SVG and HTML files cannot steal session cookies from the main application origin. Fourth: Content-Disposition: attachment on all download endpoints. Forces browser download instead of inline rendering, closing SVG and HTML execution even without a separate domain. Fifth: server-side image re-encoding via Pillow or sharp to produce a fresh image from the upload. This destroys all non-image data and eliminates polyglot payloads.

The MAGO Intel tool at intel.mago.team detects upload endpoint exposure patterns during API reconnaissance. It flags endpoints that accept multipart without Content-Disposition enforcement and serve files from the same origin as the application.

The upload endpoint accepts a file. The storage layer writes a path. When those two operations share data the client controls, the attack surface is the gap between them.

Top comments (0)