SVG upload rejected as "Invalid MIME type: application/xml." when the file starts with a DOCTYPE
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 78/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- backend
Research direction
Run the provided repro.mjs against the upload path, then inspect detectSvgFromXml and checkFileRestrictions, the entry points identified in the issue. Verify that SVGs with a standard DOCTYPE are accepted while a DOCTYPE with an internal subset remains rejected, and confirm the existing MIME restriction behavior is preserved.
Written by the indexing model from the issue text.
Description
Describe the Bug
Uploading an ordinary SVG fails if the file starts with an SVG 1.1 DOCTYPE:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10">…</svg>
The admin rejects it with Invalid MIME type: application/xml. Delete just the DOCTYPE line and the exact same file uploads fine.
This bites harder than it sounds, because a leading DOCTYPE is the default export from Illustrator, Inkscape and Affinity Designer — so for a lot of people it reads as "SVG upload is broken". The message pointing at XML rather than at the DOCTYPE makes it genuinely hard to work out what's wrong; it took me a while to stop suspecting my own config.
Where it goes wrong
file-type reports an SVG that opens with <?xml …?> as application/xml. checkFileRestrictions already handles that — it coerces back to image/svg+xml, gated on detectSvgFromXml. But that function strips XML declarations, comments and processing instructions, and then looks for the root element:
const cleanContent = content
.replace(/<\?xml[^>]*\?>/gi, '')
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<\?[^>]*\?>/g, '')
.trim()
const rootElementMatch = cleanContent.match(/^<(\w+)(?:\s|>)/)
if (!rootElementMatch || rootElementMatch[1] !== 'svg') {
return false
}
A DOCTYPE isn't in that strip list, so cleanContent still begins with <!DOCTYPE, and <! can't match <(\w+). The coercion quietly declines, the mime stays application/xml, and the allowlist check fails.
Suggested fix — one more entry in the same chain:
.replace(/<!DOCTYPE[^[>]*>/gi, '')
I'd suggest the [^[>]* bound rather than a plain [^>]*. It deliberately refuses to match a DOCTYPE carrying an internal subset, so something like <!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">]> still fails the root-element check and gets rejected, rather than being stripped and waved through. I've been running exactly this as a patch locally and it fixes the upload without loosening anything else.
Happy to open a PR if that'd help.
Link to the code that reproduces this issue
https://gist.github.com/TrimiB/005fe5d0a9a06c1bcc4b0bdfc2452e92
Reproduction Steps
npm init -y && npm i payload@3.88.0
# save the gist as repro.mjs
node repro.mjs
Output on 3.88.0:
detectSvgFromXml()
without DOCTYPE: true (expected true)
with DOCTYPE: false (expected true, actually false)
checkFileRestrictions() — what an editor actually hits
without DOCTYPE: accepted
with DOCTYPE : rejected — Invalid MIME type: application/xml.
In the admin, the same thing end to end:
- Give an upload collection a
mimeTypesallowlist that includesimage/*(this only happens whenmimeTypesis set — with no allowlist,checkFileRestrictionstakes its other branch and never sniffs the buffer). - Export an SVG from Illustrator, Inkscape or Affinity Designer with default settings, or just paste the DOCTYPE above onto any SVG.
- Upload it. You get
Invalid MIME type: application/xml. - Remove the DOCTYPE line, upload again — works.
Which area(s) are affected?
area: core
Environment Info
Payload: 3.85.1 (also reproduced against a clean install of 3.88.0, and the
source on main is unchanged)
Node: 22.22.3
Next: 16.2.6
Database: @payloadcms/db-mongodb 3.85.1
OS: macOS (darwin arm64)
- Dominant language
- TypeScript
- Stars
- 44.8k
- Forks
- 4.2k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 53
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from payloadcms/payload
-
area: ui Bug status: needs-triage v3
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
payloadcms/payload#18215 · 1 comment ·
-
Bug
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
payloadcms/payload#18163 ·
-
Bug
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
payloadcms/payload#18161 ·
-
area: core Bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
payloadcms/payload#18072 ·
-
Bug
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
payloadcms/payload#18067 · 1 comment ·
All issues in payloadcms/payload
Similar issues
-
clawsweeper:fix-shape-clear clawsweeper:queueable-fix clawsweeper:source-repro impact:ux-friction issue-rating: 🦞 diamond lobster no-stale P3
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 76/100
-
code-quality refactoring
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
github/gh-aw-firewall#8816 ·
-
integration:quickjs org:external priority:backlog topic:code-interpreter topic:middleware type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
langchain-ai/deepagents#6450 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
vercel/react-tweet#225 ·