SVG upload rejected as "Invalid MIME type: application/xml." when the file starts with a DOCTYPE

Open Beginner friendly
#17,958 0 comments 0 reactions 0 assignees View on GitHub

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

area: core Bug
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:

  1. Give an upload collection a mimeTypes allowlist that includes image/* (this only happens when mimeTypes is set — with no allowlist, checkFileRestrictions takes its other branch and never sniffs the buffer).
  2. Export an SVG from Illustrator, Inkscape or Affinity Designer with default settings, or just paste the DOCTYPE above onto any SVG.
  3. Upload it. You get Invalid MIME type: application/xml.
  4. 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from payloadcms/payload

All issues in payloadcms/payload

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.