September 22, 2026
They Allowed Five Image Formats. Four Were Pictures.
The developerβs list said images only. SVG is XML, and the browser parses it as a document.

By Nitin yadav
7 min read
Hello, I am Nitin.
Welcome to week four. Everything from here is about surfaces most hunters never touch, and we start with the one that has paid out more times than almost anything else in this series.
Let me set the scene with something I watch happen constantly.
A developer is building a profile picture upload. They know file uploads are dangerous. They have read about it. So they write a check: only images allowed. They make a list β jpg, png, gif, webp, svg β and they block everything else. Especially html. They are thinking clearly about the threat, and they block the obvious thing.
And then they ship a vulnerability, because of a fact about that list that nobody told them.
Four of those five formats are pictures. The fifth is a document.
SVG is not a bitmap. It is XML. It is a markup language, in the same family as HTML, and it supports the same kinds of dynamic behaviour β elements that respond to events, references to external resources, embedded code. When a browser opens an SVG directly, it is not decoding pixels. It is parsing a document and executing what that document tells it to.
The developer's mental model said "I allowed five image formats." The reality was "I allowed four image formats and one webpage."
That gap is today's bug, and once you see it you will find it everywhere.
The one question that decides everything
Here is what separates a real finding from a wasted afternoon, and I want it up front because most people get this wrong.
Uploading an SVG is not the bug. How the server sends it back is the bug.
You can upload a malicious SVG to a thousand applications. In most of them nothing happens, because of how the file gets served. The finding lives entirely in the response headers of the download URL.
So the moment your upload succeeds, do not celebrate. Go and fetch the file back and read the headers:
curl -sI "<https://target.com/uploads/avatar/1234.svg>"curl -sI "<https://target.com/uploads/avatar/1234.svg>"Three things in that response decide whether you have a critical or nothing at all.
Check one: the content type
If the response says the file is an SVG image type, the browser will parse it as a document when you navigate to it directly. That is the dangerous case.
If the response says plain text, or an octet-stream, or anything that is not an image or markup type, the browser will not parse it as a document. Much safer.
Check two: the disposition header
This is the one that most often saves an application without anyone realising.
If the response carries a disposition header marking the file as an attachment, the browser downloads it rather than rendering it. Even with a perfect SVG content type, nothing executes, because the document never opens in a browsing context.
If that header is absent, or says the content is inline, the browser renders it.
Check three: which origin serves it
This is what decides severity, and it is the part beginners skip.
If the file comes back from the same origin as the application β the same domain the user is logged into β then script inside that SVG runs in that origin. It can read cookies not marked HTTP-only, it can read storage, it can make authenticated requests as the user. That is a serious finding.
If it comes back from a separate asset domain with no session and no interesting data, the same script runs in an origin that holds nothing. Still worth reporting, considerably less severe.
Serving user uploads from a separate origin is the single most effective defence against this entire bug class, and teams that do it have usually thought carefully about their architecture.
So: content type says image or markup, no attachment disposition, and the same origin as the app. All three, and you have a critical. Miss any one, and you have something much smaller. Check before you invest.
Getting past the upload filter
Now the other half. Many applications do try to block this. Their checks fail in predictable ways.
The extension list that forgot. The most common case is the one from my opening β SVG is on the allowed list because it is in the mental category of images. Nothing to bypass. Just upload it.
Content-type trust. Some applications read the content type the browser declares in the upload request and trust it. That value is entirely under your control. Declare a common image type, send SVG content, and a check that never looks at the bytes will pass it.
Magic-byte checks that do not fit. A robust check reads the first bytes of the file to identify the real format. That works beautifully for binary formats, which all start with distinctive signatures. It does not work for SVG, because SVG is text and has no fixed signature β it can start with an XML declaration, a comment, whitespace, or the root element itself. Developers implementing signature checks frequently handle their binary formats correctly and then special-case SVG as "any text that parses as XML," which is not a check at all.
Extension parsing quirks. Double extensions, trailing characters, case variation, and unusual separators sometimes split differently between the validation code and the code that eventually names the stored file. Test whether what you uploaded and what got stored have the same name.
The second upload path. Same advice as day six: if the product has an import feature, a bulk uploader, or an API endpoint alongside the UI form, test all of them. Validation is frequently implemented once, in the place the developers thought about, and the other paths inherited nothing.
Conversion pipelines. Some applications convert uploads β resizing images, generating thumbnails. If SVG is passed through unconverted while raster formats get processed, that inconsistency is itself the tell that SVG took a different code path.
What to put in the file
Keep it minimal. You are proving a document executes, not building a weapon.
An SVG is XML with a root element. Inside it, the same categories of dynamic behaviour you know from HTML are available: elements that fire on load, elements that reference external resources, and embedded code blocks. Day one's principle applies unchanged β the context is a document, so you need something that runs when the document is parsed.
For proof, the standard from day one still holds: something that reveals which origin is executing. That single output is what turns your screenshot into evidence a security team can act on, because it answers their first question before they ask it.
Two practical notes:
Test with something inert first. Upload a perfectly valid, harmless SVG and confirm it renders. If the application rejects or mangles even a clean file, you know the problem is the upload path, not your payload, and you have saved yourself an hour of wondering.
Check whether the file was modified. Download what you uploaded and compare it byte for byte. Some applications run uploads through a sanitiser or a conversion library. If your file comes back altered, you are now in week three's territory β find out what changed and whether the transformation is complete.
Where to hunt, ranked by how often it works
- Avatars and profile pictures. The classic. Present in almost every product.
- Organisation or team logos. Often uploaded by an admin, rendered for everyone in the organisation. Better severity.
- Attachments in tickets, issues, comments, and chat.
- Rich text editors with image upload. Two bugs for the price of one if the editor also embeds the file.
- Document and asset libraries in anything content-management shaped.
- Import features that accept archives β sometimes the contents get extracted and served without individual validation.
- Email signature builders, which are an under-tested corner of a lot of business software.
- White-label or branding settings, where customers upload their own logo and it renders for their users.
That last one deserves attention. Branding uploads are frequently rendered on pages other tenants never see, but they are also frequently rendered in places like invoices, exported reports, and emails β surfaces that day six told you to map.
Impact ladder
- Informational β upload accepted, served with a safe content type or as an attachment. No execution. Worth a line in a report as a defence-in-depth note, nothing more.
- Low β executes, but on an isolated asset origin with no session, no storage, and no interesting data.
- Medium β executes on a sibling subdomain that shares cookie scope or is trusted by the main application in some way. Day 29 material.
- High β executes on the application origin, in a file another user will view.
- Critical β the same on a surface viewed by staff or across tenants, such as a logo rendered in an admin console or a customer-facing report.
The escalation from high to critical here is almost always about who opens the file. An avatar only you can see is self-XSS. An avatar rendered in a moderation queue is a blind XSS payload aimed at someone with more privilege than you, which is exactly where days 24 and 25 go.
Conclusion β steal this checklist
- Four of the five common image formats are pictures. SVG is a document. That gap is the entire bug class.
- Uploading is not the finding. How the file is served back is the finding. Fetch it and read the headers before investing further.
- Three checks decide everything: content type, attachment disposition, and which origin serves it. You need all three to line up.
- Serving user uploads from a separate origin is the strongest defence. Note it when you see it β it tells you the team thought about this.
- The most common bypass is no bypass at all: SVG is on the allowed list because it feels like an image.
- Declared content type in the upload request is attacker-controlled. Checks that trust it are not checks.
- Magic-byte validation does not work for SVG, because it is text with no fixed signature. Teams special-case it and the special case is usually empty.
- Test every upload path: form, API, bulk import. Validation is implemented once and the other paths inherit nothing.
- Upload a clean valid file first to separate upload problems from payload problems.
- Download what you uploaded and compare bytes. Modification means a sanitiser is in the path and week three applies.
- Hunt avatars, org logos, attachments, editor uploads, asset libraries, imports, signature builders, and branding settings.
- Severity is decided by who opens the file. An avatar in a moderation queue is a blind payload aimed at staff.
Tomorrow: everything else a file can be β filenames, metadata, PDFs, and the fields nobody thinks of as input.
If you Love reading my blogs. Check my Youtube Channel too.