WAF and Antivirus Miss File Upload Attacks Because the Payload Is a Feature
WAF and Antivirus Miss Server-Side File Upload Attacks Because the Payload Is a Feature
Most security programmes treat file upload as a solved problem. There is a WAF in front, an antivirus scanner in the middle, an extension and MIME check in the application, and a bucket at the end. Then a "critical" upload bug is disclosed in a mainstream framework and the entire assumption collapses — not because the file was malicious, but because the software that renders the file was doing something far more powerful than anyone intended.
Core assessment: dangerous upload handling is rarely about the stored object. It is about the server-side conversion engine — image optimisers, thumbnail generators, PDF and Office renderers, video transcoders — that runs attacker-controlled bytes through a parser with file, network and process capabilities. A WAF inspects transport. Antivirus inspects known-bad content. Neither inspects what your own converter is able to do with a syntactically perfect file. That is the blind spot, and it is structural, not a tuning problem.
Two Different Tools, Two Different Failures
Conflating WAF and antivirus is why organisations overestimate their coverage. They fail for unrelated reasons.
A WAF is a transport inspector. It matches request strings against signatures, rate-limits, checks IP reputation and normalises encodings. Its unit of analysis is the HTTP request. Once a multipart body has been accepted and de-serialized into a temp file, the WAF has finished its job. It does not know that your application is about to hand that file to a converter with 200 format handlers, some of which can read from disk, write to disk, or make outbound requests. WAF rules that try to express "SVG whose text content references an MSL script that writes to an absolute path" don't exist, because the match isn't a string — it's a behaviour chain inside a different process.
Antivirus is a content matcher. It answers "have I seen these bytes, or bytes shaped like these, before?" A format-exploit payload is usually not malware. It's a valid image container, a legal vector document, or a compressed stream whose header fields overflow an internal buffer. Cleaning a legitimate file is what antivirus refuses to do, by design. It also typically runs on the object at rest or on delivery, not during the parse — so the exploit fires while the scanner is idle.
The consequence: the same control appears twice in the audit and protects against neither. Coverage claims should be written as "we detect known-bad content and known-bad request patterns", not "we prevent upload exploitation".
What A Conversion Engine Actually Exposes
Media toolkits grew for decades by adding formats, and each format brought behaviour. Grouped by capability:
Format coders. Image and document toolkits register a handler per format. Some handlers were never meant for hostile input and interpret directives inside the file: layout/script formats that can rename, move or write files; vector formats that embed external references; compressed or container formats that carry path metadata. A single accepted upload type can transitively enable dozens of secondary parsers, because the engine follows internal references to decode the object.
Delegates. Most engines shell out to specialised libraries for formats they don't handle natively — document rasterisation, font rendering, raw camera formats, video streams. Each delegate is a separate binary with its own attack surface and, crucially, its own privileges. Patching the wrapper doesn't patch the delegate.
Protocol handlers. Converters frequently accept a reference instead of bytes: remote image URLs in CSS or vector documents, archive members, stylesheet locations. That turns your image pipeline into a server-side request forgery client with an unusual fingerprint — able to reach internal metadata services, and often able to exfiltrate by encoding the response into a rendered artefact.
Output naming. The output path, temp file names and cache keys frequently include attacker-influenced values (the original filename, the requested transformation name, a derived identifier). Path traversal here is not an upload bug in the classic sense; it's a file-naming bug that only becomes reachable because uploads are attacker-controlled.
Resource limits. Decompression ratios, canvas dimensions, tile counts and recursion depth are attack knobs. A small file can expand into a huge allocation. This class turns an image upload into a queue-stalling denial of service, or into a timing oracle that proves a parse path executed.
The Three Shapes This Class Takes
In practice upload exploitation resolves into three reproducible shapes:
- Format-driven read. The parser is persuaded to include bytes from outside the intended object — a config file, a key, a local service response — and to embed them in the rendered output. The output is the exfiltration channel; no additional network access is required.
- Format-driven write. The parser is persuaded to write attacker-controlled bytes to an attacker-chosen path. In web applications, a write primitive plus any publicly reachable and executable location equals code execution. This is the shape that produces framework-level critical advisories.
- Pipeline SSRF. The engine is given a reference and dutifully fetches it. Because the fetch happens inside a conversion process, it usually has a different network position and a different egress policy than your web tier — which is exactly why it can reach things your request validation was designed to block.
All three bypass the WAF, because the request is well-formed. All three bypass antivirus, because the file is well-formed. The vulnerability is the grant of capability to the parser.
Proving It During An Assessment
Claims without evidence are noise, and teams correctly retest these findings. The method:
- Differential parse. Submit the same logical image through every accepted input type that reaches the engine. If behaviour changes with the container, secondary parsers are in scope and the surface is bigger than the product owner believes.
- Canary read. Point the parser at a file whose content is non-secret but verifiable, and check whether it appears anywhere in the rendered artefact, its metadata or the response size. Presence means an inclusion primitive.
- Blind verification. Where nothing returns, use an out-of-band callback on a distinct DNS/HTTP channel and attribute the request to the conversion worker rather than the web tier. A callback from the converter proves both the primitive and the network position.
- Bounded write. Never drop a web shell during an authorised test. Write a uniquely named inert file to a predictable path, prove it lands, prove whether it is served and whether it is executable, then remove it. The evidence requirement is "arbitrary write", not "compromise".
- Policy fingerprinting. Query the engine's version and its effective policy: which formats are enabled, which delegates are resolvable, whether network access is permitted, whether the filesystem is mounted read-only. Findings should be reproducible from the artefact, not from an assumption about defaults.
- Async blast radius. Uploads that are processed by background workers must be checked in the worker's context. A path that is safe in the request/response process may be privileged in the converter.
Hardening, Ordered By Reliability
1. Stop trusting the container. Validate by magic bytes and by decode success, then treat the decoded content as the only object that exists. Extension, declared MIME and content-sniffing library output are hints, not authorisation.
2. Re-encode to a canonical format by parsing pixels, not by delegating. The most reliable control in this class is: decode the image to raw pixels, discard everything else, and re-emit a fresh file. This destroys embedded directives, metadata, external references and secondary format payloads in one step. Vector and document formats cannot take this shortcut and should be handled in a separate, more isolated lane.
3. Disable what you don't render. An engine configured to decode one or two formats is an order of magnitude smaller surface than a default install. This is a configuration control with a very high effect-to-effort ratio and it must be re-verified after upgrades, because upgrades routinely re-enable delegates.
4. Isolate the worker. Separate host or container, no credentials, read-only root filesystem, no outbound network, CPU and memory and wall-clock and output-size limits, dropped capabilities, no access to application secrets, and an ephemeral scratch directory on its own mount. Assume the parser will be exploited and design the neighbours.
5. Own the output path. Generate filenames and keys server-side. Never derive a path from user input. Enforce a prefix that cannot be escaped, and validate the final resolved path after normalisation.
6. Separate storage from execution. Uploads land in private object storage, never in a served document root. If files are ever materialised on disk, that location must not be executable, must not be static-served, and must be served with Content-Disposition and a restrictive content type.
7. Gate the async transition. A queued file is unscanned and unverified. Application state must never treat "accepted" as "safe", and previews must not be produced from a state that implies trust.
8. Instrument and version-pin. Record the exact version and effective policy of every engine and delegate. A file-upload control that isn't version-observed silently regresses on every base-image update.
Decision Table
| Situation | Exposure | What to do first |
|---|---|---|
| Converter runs in the web process, same privileges | Remote file write becomes app compromise | Move conversion to an isolated worker before anything else |
| WAF plus antivirus is the stated upload control | Neither sees parse-time behaviour | Add decode-and-re-encode, disable delegates, isolate |
| Document or vector rendering (Office, PDF, SVG) | Highest-capability parsers in the stack | Air-gapped render lane, no egress, dedicated output bucket |
| User-supplied URLs accepted in a field | Pipeline SSRF and internal port mapping | Allow-list by resolved IP after redirect, block link-local ranges |
| Feature already public with unknown history | Assume inclusion and write primitives existed | Retain logs, hunt for uniquely named artefacts on disk, review egress logs |
Checklist
- Upload type validation uses magic bytes plus successful decode.
- Images are re-encoded to a canonical format; embedded directives cannot survive.
- Only the required format handlers are enabled, verified after each upgrade.
- Delegates are inventoried with versions, and unnecessary ones are absent.
- Conversion runs in an isolated worker with no credentials and no egress.
- Read-only root filesystem, scratch on its own mount, CPU/memory/time/output caps set.
- Filenames and storage keys are server-generated, with post-normalisation path checks.
- Uploaded objects live in private storage, never a served document root.
- Queue state is distinguishable from "verified" state in the application model.
- Engine versions and effective policy are recorded as deployment artefacts.
- Out-of-band verification exists for blind parse paths.
- A retest criteria document exists for each closed finding.
When This Needs External Review
When three conditions hold: uploads are unauthenticated or widely authenticated, files are converted by any third-party media or document engine, and nobody in the team can name the engine's enabled formats and delegates. That combination means the capability granted to the parser has never been examined — which is where this class lives. Eresus Security tests this path end to end and delivers a reproducible artefact, the affected worker context, the privilege boundary that failed, and a retest criterion. The adjacent disciplines are covered by file upload pipeline testing, web application penetration testing and API security testing.
FAQ
Doesn't a good WAF rule set stop malicious uploads?
A WAF can stop obvious transport-level abuse: oversized bodies, malformed multipart framing, known exploit strings in a query parameter, suspicious rates. It cannot express "this PNG is structurally valid but makes the renderer write a file outside its scratch directory", because the malicious part is a legitimate format feature consumed by a different process. Once the request is accepted, the WAF's model of the attack ends.
If I run antivirus on every upload, am I covered?
You are covered for known-bad content. You are not covered for parser exploitation, resource exhaustion, path traversal in output naming, or server-side requests initiated by the engine — none of which look like malware to a scanner. Antivirus is worth keeping, but as a content filter for what you store and distribute, not as a substitute for parse isolation.
Is disabling formats and delegates enough on its own?
It reduces the surface substantially and cheaply, which is why it ranks high. It is not sufficient alone: remaining handlers and delegates still run with whatever privileges, filesystem view and network position the process has. Isolation is what bounds the consequence when a retained handler is eventually found exploitable.
Can automated scanners find this class reliably?
Scanners are useful for the cheap signals — missing validation, oversized uploads, storage permission errors, obvious SSRF from URL fields. They are weak on this class because proof requires format-specific construction, access to the rendered artefact, and often out-of-band attribution across an asynchronous worker boundary. That is a manual, evidence-driven test.
Next Step
Continue with why a WAF alone will not stop exploitation, then HTTP method variance and WAF bypass for the transport-side blind spots, and the pipeline view of uploads for storage, preview and download authorization.
AI Security Starter Training
Request a practical checklist for prompt injection, RAG data leakage, MCP risks, and model-file security before launch.
Related Research
The Limitations of WAF: Why Firewalls Alone Can't Prevent Hacks
Your company relies on a pricey WAF (Web Application Firewall) to block threats. But why is a WAF completely blind to logical flaws? Discover how manual...
Application SecurityFile Upload Security: Testing the Pipeline End to End
A practical review of validation, parser isolation, storage, download authorization, scanning, and asynchronous upload pipelines.
Web SecurityRFC 10008's New HTTP QUERY Method Is a Blind Spot for Every WAF Written Before June 2026
The IETF standardised a new HTTP method — QUERY — that behaves like a GET/POST hybrid. Security stacks built around GET/POST/PUT/DELETE assumptions now have a gap to close.
Application SecurityDAST Findings Need More Than a Severity Score: HTTP Evidence and OAST Callbacks
A practical method for reviewing DAST findings with the request, response, scope, and out-of-band evidence that engineering needs to reproduce them.
Related Services
Scope Estimator
Get a rough engagement size before the scoping call.
Estimated engagement
5–7 days