File upload vulnerability exploitation: the chains that produce real compromise

Key takeaways
Direct web shell execution is the file upload attack most validation stacks are built for -- in Vulnox assessments it is also the least common path to serious compromise; chained attacks via SSRF, stored XSS, and path traversal survive content-type validation and libmagic checks.
SVG files pass every standard content validation check and execute inline JavaScript when served from the application origin -- a valid SVG is a stored XSS payload if the application serves uploads from its own domain.
SSRF via file upload reached AWS instance metadata credentials in three separate Vulnox assessment engagements involving document preview functionality -- the upload endpoint was external, the SSRF target was the internal metadata service.
Polyglot files are simultaneously valid in two formats -- a JPEG/PHP polyglot passes libmagic as a genuine JPEG while containing executable PHP code, bypassing content sniffing because validation and execution use different parsers.
S3 migration removes web shell execution risk and introduces presigned URL misconfiguration, predictable object key exposure, and server-side processing pipeline risks that are less documented and less defended.
Path traversal via unsanitized filenames passed to os.path.join or equivalent is present in applications that correctly validate extension and content type but never sanitize the filename string itself.
TL;DR
File upload defenses are almost entirely optimized for one attack: a PHP webshell with a disguised extension executed directly on the web server. That attack is well-understood, well-documented, and increasingly well-defended. The chains that produce serious compromise in current assessments go through the upload endpoint toward something else -- an internal service via SSRF, a stored XSS payload via SVG, a filesystem write via path traversal in the filename. The validation stack catches the obvious case. The chains survive it.
The document preview feature that reached the metadata service
A SaaS platform in the UK offered document preview functionality -- users uploaded PDFs and Office files, the application rendered a preview image server-side using a processing library. Upload validation was solid: libmagic content sniffing, extension allowlist, 10MB size limit, uploads stored in S3 with randomized keys. The validation stack would have caught a PHP webshell. We uploaded a crafted DOCX file containing an external OLE object reference pointing to http://169.254.169.254/latest/meta-data/iam/security-credentials/. The document processor followed the reference during preview generation. The response contained the IAM role credentials for the EC2 instance running the processing job. The credentials had S3 write permissions to the production artifact bucket.
The upload validation was well-implemented for the attack it was designed to catch. The document processor that consumed the validated file was running with production IAM permissions and followed external URLs without restriction. The file was genuinely a valid DOCX. It passed every check. The attack surface was not in the upload handler -- it was in what happened to the file after the handler finished.
Why the chains that matter survive content validation
Content-type validation and content sniffing answer one question: is this file structurally what it claims to be? A libmagic check on an SVG file confirms it has valid XML structure and SVG namespace declarations. It does not evaluate whether the SVG contains script elements, external resource references, or event handler attributes that execute JavaScript. A libmagic check on a PDF confirms the PDF header and cross-reference table. It does not parse every object in the PDF for JavaScript actions or external URI references that the PDF renderer will follow. The validation and the execution happen in different layers with different parsers, and the validation layer has no visibility into what the execution layer will do with a structurally valid file.
Example
An SVG file that passes libmagic as image/svg+xml can contain this structure: an svg element with an onload attribute calling fetch() against an attacker-controlled endpoint, passing document.cookie as a query parameter. When a user or administrator views the uploaded file and the application serves it from the application origin with content-type image/svg+xml, the browser renders it as an SVG and executes the JavaScript in the origin context. The attacker gets the session cookie. The application served a file it had validated as a legitimate image. Both things are true simultaneously.
The same pattern applies to PDF JavaScript actions (executed by Acrobat and some browser PDF renderers), Office macro content (executed during processing by LibreOffice-based converters), and ZIP/tar path traversal (executed during extraction by archive processing code). In each case the file format has a legitimate processing behavior that the validation step does not model. The attack lives in the gap between what the format allows and what the validator checks.
What the assessment data shows about real file upload exploitation
Assessment base: File upload testing across 91 web application assessments, 2022-2024, mix of SaaS, fintech, healthcare, and e-commerce applications
SSRF via document processing reached internal services in 11 of 34 applications with server-side file processing
In applications that processed uploaded documents server-side -- PDF preview, Office conversion, image resizing, archive extraction -- SSRF via crafted file content was testable in 34 of 91 assessments. Of those 34, 11 produced SSRF responses from internal addresses. Three reached the AWS instance metadata endpoint at 169.254.169.254. Two reached internal APIs bound to localhost. Six reached other internal services on the same VPC subnet. In all 11 cases the upload validation stack was functioning correctly and the uploaded files were structurally valid instances of their declared format.
Server-side processing of uploaded files is an implicit trust decision. The application validates the file and then hands it to a processing library that may make network requests, follow external references, or expand archive contents. The processing library's behavior is the attack surface, not the upload handler. Applications that validate uploads and then process them server-side need to network-isolate the processing environment and restrict outbound connections from processing jobs, independent of how strong the upload validation is.
SVG upload to stored XSS in 8 of 23 applications that allowed SVG uploads
Of 91 assessments, 23 applications allowed SVG file uploads -- avatar uploads, icon uploads, asset management systems, design tools. In 8 of those 23, SVG files containing script content were served from the application origin with content-type image/svg+xml or without a content-type that forced download behavior. The script executed in the application origin context on view. In 5 of those 8, the uploaded SVG was visible to other users -- profile avatars, shared asset libraries -- making it a persistent stored XSS payload accessible to anyone who viewed the content.
SVG is an image format that the browser treats as HTML for script execution purposes when served inline. The practical defense is to serve all user-uploaded files from a separate domain or subdomain that does not share the application's cookie domain, and to set Content-Disposition: attachment on all uploaded file responses. Both controls are frequently missing in applications that moved uploads to S3 but serve them via a same-origin proxy or CloudFront distribution on the application domain.
Path traversal via filename in 14 of 91 assessments, all in applications with otherwise functional content validation
Filename sanitization failures appeared in applications that correctly validated content type and file extension but passed the raw filename string to storage path construction. The applications had invested in upload validation and considered the input validated after the content checks passed. Filename sanitization was treated as a separate concern from content validation -- in 14 cases it had not been implemented. Payloads that survived included null-byte injection (filename.jpg%00.php in older PHP environments), Unicode normalization attacks producing path separators after normalization, and straightforward ../ sequences that the storage library did not reject.
Content validation and filename sanitization are independent controls that need to be implemented independently. Passing content validation does not mean the filename is safe to use in path construction. The two operations use different inputs -- the file content and the filename string -- and both need to be sanitized. In code review, path traversal via filename is found by tracing the filename variable from the upload handler to any storage.put(), open(), or path.join() call and verifying normalization and allowlist enforcement happens in between.
Presigned URL misconfiguration allowed arbitrary content type on authenticated upload endpoints in 9 of 31 S3-backed applications
Applications that migrated uploads to S3 commonly use presigned URLs to allow clients to upload directly to S3 without proxying through the application server. In 9 of 31 S3-backed applications assessed, the presigned URL generation did not specify a content-type condition on the presigned policy. S3 presigned PUT URLs that do not specify a content-type condition in the policy allow the client to upload any content with any content-type header. The application generated the presigned URL after its own validation logic determined the upload was acceptable. The client then used the presigned URL to upload different content than was validated. The application never saw the actual uploaded bytes.
Presigned URL generation moves the upload out of the application's request path, which was the original motivation -- reduce server load, allow direct client-to-S3 upload. The side effect is that the application validates a filename or metadata, generates a presigned URL based on that validation, and the client can then use that URL to upload anything. Content-type conditions in S3 bucket policies and presigned URL policies are the control that closes this gap, and they are non-default in most S3 SDK presigned URL implementations.
Better content validation correlates with worse exploitation outcomes in some environments
Common belief
Stronger upload validation -- libmagic content sniffing, strict allowlists, AV scanning -- reduces the risk of serious file upload exploitation.
What we found
In one assessment, an application with libmagic validation, AV scanning, and an extension allowlist served all validated uploads from the application origin with inline content-type headers, processed them server-side in a container with full internet egress, and constructed storage paths using the raw client-supplied filename. The validation stack was the strongest in the assessment set. The downstream handling was among the weakest. The SSRF and stored XSS chains both worked. The path traversal worked. The validation had been implemented thoroughly and then the team had moved on.
Stronger validation on the upload handler increases developer confidence that the upload surface is handled. That confidence sometimes results in reduced scrutiny of what happens to validated files downstream -- the processing pipeline, the serving configuration, the filename handling. In several Vulnox assessments, the applications with the most rigorous upload validation had the most permissive downstream processing configurations because the development team had invested attention in the validation layer and treated the rest of the pipeline as covered. The applications with minimal upload validation sometimes had more conservative downstream configurations -- separate serving domains, restricted processing environments -- because the team knew they had not fully solved the upload problem and compensated elsewhere.
What development teams say about their upload security and what the code shows
'We use libmagic for content sniffing and reject anything that does not match the allowlist. Our upload validation is solid.'
Root cause:libmagic checks the file signature against known format patterns. It does not evaluate the semantic content of structurally valid files. An SVG with inline script, a PDF with JavaScript actions, a DOCX with external OLE references -- all pass libmagic as valid instances of their format. The validation is correct for what it tests. What it does not test is whether a structurally valid file contains payloads that downstream processors will execute. The statement is accurate and incomplete at the same time, which is the most dangerous kind of accurate.
'We moved everything to S3. There is no server executing uploaded files anymore.'
Root cause:S3 does not execute PHP or server-side code. It does not process documents, render previews, or extract archives. If the application does any of those things -- and most applications that accept meaningful file types do -- those operations happen in compute resources that consume the S3-stored file. The S3 migration removed web shell execution at the web server. It did not remove the processing pipeline that handles the file after storage. The attack surface moved from the web server to the processing job, and processing jobs frequently run with broader permissions than web server processes because they need to write results back to databases and storage systems.
'We validate the filename and strip special characters before storage.'
Root cause:Stripping special characters is not equivalent to allowlist-based sanitization. Most stripping implementations remove characters from a denylist -- removing '/', '.', '..' as literal strings. Unicode normalization attacks produce path separator characters after normalization from sequences that the denylist did not match pre-normalization. Null byte injection terminates the string at the null byte in some runtime environments, producing a different effective filename than the one that passed the strip operation. The correct implementation is to generate a random storage key server-side and discard the client-supplied filename entirely for storage path construction, preserving the original filename only in metadata for display purposes.
The file upload attack surfaces that testing guides do not cover
ZIP and archive expansion path traversal in import pipelines
Applications that accept ZIP or tar archives for bulk import -- data imports, theme uploads, plugin installations -- frequently extract the archive server-side using libraries that do not validate extraction paths against the destination directory. A crafted ZIP file containing entries with filenames like ../../etc/cron.d/backdoor writes files outside the intended extraction directory during unzip operations that use the entry filename directly. This is a well-documented class (Zip Slip) but appears consistently in custom import implementations that do not use extraction libraries with path traversal protection. It survives content-type validation because the ZIP is a valid ZIP. The attack is in the entry filenames inside the archive, which the outer validation never inspects.
Uploaded file processing in the same execution context as application logic
Processing jobs that handle uploaded files -- image resizing, document conversion, archive extraction -- frequently run in the same container, Lambda function, or server process as the application itself. An XXE or SSRF triggered during file processing has access to the same environment variables, filesystem, and network as the application. This is an architecture choice that is almost never documented as a security decision -- it is a convenience choice made during development. The attack surface of a processing pipeline is the union of the attack surface of the uploaded file formats it handles and the permissions of the execution context, which is often broader than the web server context.
Content-Disposition absence on file serving endpoints
Serving uploaded files without Content-Disposition: attachment from the application origin allows the browser to render any file format it recognizes inline. This is the enabling condition for SVG stored XSS, HTML file injection, and some PDF JavaScript execution paths. The correct configuration is to serve all user-uploaded files either from a separate origin that does not share the application cookie domain, or with Content-Disposition: attachment forcing download behavior. Neither is default in most web frameworks. Applications that correctly validate uploads on ingress and then serve them without disposition headers have implemented half the control.
Where file upload exploitation is heading
Within 18 months, AI document processing endpoints will become the primary high-value target for SSRF via file upload, displacing traditional document preview pipelines as the most accessible path to internal credential access.
AI document processing features -- PDF summarization, contract analysis, image description -- are being added to SaaS applications at high speed. These features involve server-side processing of uploaded files by AI inference infrastructure that frequently runs with broader cloud permissions than traditional document preview services, because the AI layer needs to write results to storage and databases. The SSRF attack surface is structurally identical to traditional document processing SSRF. The permissions available on compromise are higher because AI inference infrastructure is newer and often over-provisioned relative to least-privilege targets. The attack technique is already documented. The target has become more valuable.
Confidence: highPublished CVEs or bug bounty disclosures specifically attributing SSRF to AI document processing endpoints, distinct from traditional document preview functionality, reaching internal cloud credentials. Watch HackerOne disclosure feeds and CVE descriptions mentioning LLM or AI document processing contexts before Q4 2026.Stored XSS via SVG upload will be identified as a distinct vulnerability class in a major framework's security documentation within 24 months, prompting default Content-Disposition header enforcement in at least two major web frameworks.
SVG stored XSS is currently treated as a misconfiguration rather than a vulnerability class, which means it appears in security assessments but not in framework security defaults. As more applications add user-generated content features and SVG support becomes more common in design and media tools, the frequency of findings will increase to the point where framework maintainers address it in defaults rather than documentation. The pattern of a vulnerability class reaching framework-level default remediation follows a predictable path: scattered findings, security advisory, framework documentation, finally default behavior change.
Confidence: mediumA security advisory or framework changelog from Django, Rails, Laravel, or Next.js specifically addressing SVG upload XSS as a default content-type or content-disposition behavior change, published before Q2 2027.
Why file upload testing stops too early
Most file upload security testing ends at the upload handler. The tester uploads a PHP shell, confirms it does or does not execute, marks the control as pass or fail, and moves on. That testing answers one question about one attack pattern. The chains that produce serious compromise in current assessments start at the upload handler and end somewhere else entirely -- in the processing pipeline, in the serving configuration, in the archive extractor. Testing the upload handler in isolation is like testing a door lock without checking whether the window next to it opens. The handler is one node in a data flow that spans multiple components, and the vulnerability is frequently not in the node that was tested.
Counterargument
The counterargument is that thorough data flow analysis for every upload endpoint is expensive and most assessments have limited time. That is correct. The practical version of this position is not that every upload requires full pipeline analysis -- it is that the test plan should explicitly include at least one downstream check for every validated upload endpoint: does the application serve uploaded files from the application origin, does it process them server-side, does it extract archives, does it pass filenames to path construction. Those four questions take 20 minutes to answer per endpoint and catch the majority of the chains that the upload handler test misses.
What the defense stack needs that most implementations are missing
Serving uploaded files from a separate origin (step 2) is skipped most consistently because it requires DNS configuration, separate storage bucket policy, and sometimes CDN changes that involve multiple teams. The alternative -- Content-Disposition: attachment as a response header -- is a one-line change in the file serving endpoint and achieves most of the same protection for the stored XSS chain. Teams that cannot implement the separate origin in the short term should implement the Content-Disposition header immediately. It is the control with the highest return on the lowest implementation effort in this stack.
- 1Developer
Generate a random storage key server-side for every uploaded file and discard the client-supplied filename for path construction entirely. Store the original filename in metadata for display only. Never pass the client-supplied filename to any filesystem, storage, or archive operation. This closes path traversal and null-byte injection regardless of what sanitization is or is not applied to the filename string.
Expected outcomePath traversal via filename is eliminated. The storage path is under application control at all times. Client-supplied filename manipulation has no effect on where the file is written.
- 2Developer / operations
Serve all user-uploaded files from a separate origin that does not share the application cookie domain, with Content-Disposition: attachment on all responses. If a separate origin is not feasible, Content-Disposition: attachment on the serving endpoint prevents inline browser rendering for all file types including SVG. This closes stored XSS via SVG, HTML file injection, and most PDF JavaScript execution paths.
Expected outcomeUploaded files cannot execute in the application origin context regardless of their content. SVG script execution, HTML injection, and PDF JavaScript are all prevented by the response headers.
- 3Operations / cloud infrastructure
If using presigned S3 URLs for direct client upload, include a content-type condition in the presigned URL policy matching the content-type determined during server-side validation. S3 will reject PUT requests where the Content-Type header does not match the policy condition. This prevents the presigned URL from being reused to upload different content than was validated.
Expected outcomePresigned URL reuse for content substitution is prevented. The S3 object content-type is constrained to what the application validated.
- 4Operations / cloud infrastructure
Run file processing pipelines -- document preview, archive extraction, image processing -- in isolated execution environments with no internet egress and minimal IAM permissions. Specifically: deny all outbound connections except to required endpoints, use separate IAM roles for processing jobs with no access to production application credentials, and run processing in ephemeral containers that are destroyed after each job. This limits the blast radius of SSRF and XXE triggered during file processing.
Expected outcomeSSRF triggered in the processing pipeline cannot reach internet-accessible attacker infrastructure or AWS metadata endpoints. Credential access from a compromised processing job is limited to the processing role scope.
One thing to check in your application this week
Find every endpoint in your application that serves user-uploaded files and check the response headers. Specifically: is the Content-Disposition header set to attachment, and is the file being served from the application origin domain or a separate domain? If user-uploaded files are served inline from the application origin -- which is the default behavior in most storage proxy implementations -- and your application accepts SVG, HTML, or any document format, you have a stored XSS surface regardless of how strong your upload validation is. Check the serving configuration, not the upload handler. The upload handler is probably the part that was reviewed. The serving configuration is probably the part that was not.
Further Reading
Vulnerability Assessment
comprehensive vulnerability assessmentDigital Footprint
digital footprint analysisOWASP Web Security Testing Guide
OWASP Web Security GuideNIST Vulnerability Assessment Definition
NIST's vulnerability assessment definitionNVD Vulnerabilities Database
NIST Vulnerability Database
Frequently Asked Questions
What is the most dangerous file upload exploitation chain that bypasses standard defenses?
The chain that survives the most defenses starts with an upload that passes content-type validation and libmagic checks -- typically a valid SVG file or a valid PDF -- and uses the file as a delivery mechanism for stored XSS, SSRF, or XML external entity injection rather than direct code execution. An SVG file is valid XML. Browsers render inline script in SVG when served from the application origin. An upload validation stack that correctly identifies the file as image/svg+xml and allows it has still accepted a stored XSS payload if the application serves the file from its own domain. This chain survives content sniffing because the file is genuinely what it claims to be.
How do attackers use file uploads to reach internal services via SSRF?
Several file formats trigger server-side URL fetching during processing. PDF renderers fetch linked resources. Office document processors follow hyperlinks during preview generation. ImageMagick and similar libraries follow URLs in certain image format directives. An attacker who uploads a crafted file to an application that processes uploads server-side can cause the application server to make HTTP requests to internal addresses -- including AWS instance metadata endpoints, internal APIs, and services bound to localhost. The upload endpoint is external-facing. The SSRF target is internal. The file content is the bridge. In Vulnox assessments, SSRF via file upload reached AWS metadata credentials in three separate engagements involving document preview functionality.
Why does moving file uploads to S3 not eliminate file upload vulnerabilities?
S3 migration removes the web shell execution risk -- PHP does not execute in object storage. It introduces a different set of risks that are less well-documented: predictable object key naming that allows unauthenticated access to other users' uploaded files, presigned URL misconfigurations that allow upload of arbitrary content types to authenticated-upload endpoints, public bucket ACLs that allow uploaded files to be served directly to browsers from the S3 domain triggering CORS or content-type sniffing issues, and server-side processing pipelines that consume uploaded files and reintroduce code execution risk in the processing layer. The S3 migration changes where the risk lives, not whether it exists.
What does a file upload path traversal attack look like and what does it achieve?
Path traversal via filename occurs when an application uses the client-supplied filename in the storage path without sanitization. A filename like '../../../etc/cron.d/backdoor' -- URL-encoded as needed to survive HTTP transport -- can write the uploaded file to an arbitrary filesystem location if the storage logic constructs the path by concatenating the upload directory with the raw filename. The outcome depends on where the file lands: overwriting application configuration files, writing to web-accessible directories outside the intended upload path, or placing files in locations the application subsequently executes or includes. Vulnox assessments find this in applications that sanitize file extension and content type but pass the raw filename to os.path.join or equivalent.
How do file upload vulnerabilities reach CI/CD pipelines?
Applications that accept file uploads and subsequently process those files in automated pipelines -- document conversion, media transcoding, static analysis, import processing -- often pass the uploaded file to a processing job that runs in the same environment as or with access to the build infrastructure. A malicious archive (ZIP, tar) containing path traversal sequences can write files to locations the processing job accesses. A malicious document that triggers XXE in the processor can reach internal services. In one Vulnox assessment, an import file processed by a data pipeline running in the same Kubernetes namespace as the CI/CD tooling allowed lateral movement from the upload handler to the build agent through a combination of XXE-triggered SSRF and an overly permissive service account binding.
What SAST rules actually catch file upload vulnerabilities that scanners miss?
Standard SAST rules for file upload vulnerabilities check for absent extension validation and missing content-type checks. Those rules catch the simplest cases. The chains that matter require rules that trace data flow from the upload handler through to storage path construction (catching path traversal), through to URL-fetching library calls (catching SSRF), and through to template rendering or file inclusion calls (catching stored XSS and code inclusion). Taint-tracking rules that follow the uploaded filename and file content through the application code surface these chains. Most default SAST rulesets do not include taint-tracking rules for upload data flow beyond the immediate validation boundary.
What is the polyglot file technique and why does it bypass content sniffing?
A polyglot file is a file that is simultaneously valid in two different formats. A JPEG/PHP polyglot, for example, is a file that passes libmagic as a valid JPEG -- the magic bytes are correct, the JPEG structure is intact -- but also contains valid PHP code in a section that the JPEG parser ignores. If the application validates content using libmagic and then stores the file with a .php extension (or if the storage path allows extension control), the file executes as PHP despite passing the content sniff. The polyglot survives the validation step because the validation checks what the file starts with, not whether all parseable sections of the file are safe. Content sniffing is a necessary control but not a sufficient one when the file is subsequently processed by a different parser than the one used for validation.
Related Articles

API security assessment: what automated tools miss and manual testing finds
Automated API security tools find the vulnerabilities they were designed to look for. The findings that matter in real assessments, broken object-level authorization, JWT algorithm confusion, and unauthenticated internal endpoints, require a tester who understands what the API is supposed to do before probing what it actually does.

CVSS score explained: what the metric captures, what it misses, and how to fix your prioritization
In 40% of Vulnox assessments, vulnerabilities scored CVSS 9.0 or higher were on systems with compensating controls that materially reduced the actual risk. The score was accurate. The priority order it implied was not. This guide covers how to use CVSS correctly and what to layer on top of it.

Post-breach vulnerability assessment: what the attacker left behind
Most post-breach assessments start too late and end too early. They confirm what was hit, not what the attacker mapped before they moved. The second breach usually comes from the reconnaissance that happened in the first.
Ready to Secure Your Digital Assets?
Get a comprehensive vulnerability assessment for your website today.