[Bug]: Editing a Markdown file with inline (data: URI) images deletes them from the file
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 659
- Forks
- 133
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 110
Description
⚠️ This issue respects the following points: ⚠️
- This is not a troubleshooting question, general support matter, or webserver/proxy problem, but likely a bug (if unsure, ask the Community Help Forum).
- This issue is not already reported on Github OR solved at the Community Help Forum (I've searched!).
- I'm using a maintained major version of Nextcloud Server and tested against the latest patch level. (Supported major versions and current patch levels).
- I agree to follow Nextcloud's Code of Conduct.
- I've tried my best to provide clear reproduction steps that someone unfamiliar with this bug could use to reproduce it.
Bug description
Text does not render Markdown images whose source is a data: URI, and — more seriously — once
such a document is opened and edited, the images are removed from the stored file. They are not
shown as broken, or as a placeholder: the image is absent from the document Text builds, so the
next save writes the file back without it. A one-word text edit is enough to destroy every picture
in the document.
Inline data: images are valid Markdown (CommonMark places any URL in ), and they are
the natural format for self-contained documents — a single file that stays complete when it is
moved, copied, shared, or read offline. Several tools export chats, notes and reports this way.
Steps to reproduce
- Create a Markdown file containing one inline image, e.g.
— a normal JPEG, nothing exotic. - Upload it to Nextcloud (WebDAV or the Files web UI). Confirm the file on the server still
contains thedata:URI. - Open it in Files. Text displays the document — the image is missing, with no indication
that anything was omitted. - Change one word of the text and let Text save.
- Download the file, or inspect it on the server.
Expected behavior
Either the image renders, or — at minimum — content Text cannot represent is preserved on save.
Silently discarding part of a user's file is the part that matters: the document was complete when
it was opened and is not when it is closed.
Actual behaviour
The image is gone from the stored file.
Measured on a live instance (sizes and MD5s of the same document before and after):
| copy | size | md5 |
|---|---|---|
| as uploaded | 56344 | ad73141d3179f82d35e97bf482fe23b7 |
| after opening in Text and editing one line | 2888 | 97f7f445021d9b82fbaa4ff015ae3757 |
The  line is not altered or replaced — it is deleted. Nextcloud's
own file versioning kept the original, which is the only reason nothing was lost; a user without
versions enabled, or one who notices after the retention window, loses the images permanently.
Where it comes from
Text builds image nodes from markdown-it tokens via the image node's parseMarkdown, and the
TipTap Image extension it uses declares:
addOptions() { return { inline: false, allowBase64: false, HTMLAttributes: {}, resize: false } }
…
parseHTML() { return [{ tag: this.options.allowBase64 ? "img[src]" : 'img[src]:not([src^="data:"])' }] }
allowBase64 is TipTap's own default and Text never sets it, so data: sources are excluded. The
node is therefore never created; the document Text holds has no image in it; and the Markdown
serializer, which writes what the document contains, emits a file without the image.
Worth noting that every other layer already accepts these images:
- markdown-it, which Text bundles, explicitly permits them — its own validator is
/^data:image\/(gif|png|jpeg|webp);/, i.e. raster images pass anddata:image/svg+xmldoes not. - Nextcloud's CSP already allows
data:images (img-src * data: blob:).
So the exclusion is not a considered security position — it is an unset option, inherited from a
library default, that happens to also delete user data.
Suggested fix
- Never drop unrepresentable content on save. Whatever is decided about rendering, a document
that arrives with content Text cannot display should not be written back without it. This is the
data-loss half and it is independent of the rest. - Allow inert raster data URIs. Enable base64 images for
data:image/png,image/jpeg,
image/gif,image/webpand keep rejectingdata:image/svg+xml, which is the only genuinely
dangerous case (SVG can carry script). markdown-it's existing allow-list is exactly this set, so
the two layers would finally agree. - If inline images are unwanted as a matter of product design, then say so in the UI — a
placeholder reading "1 image not displayed" would at least warn the user before they edit and
lose it.
Workaround (for anyone finding this issue)
Removing the guard from the built chunk restores both rendering and round-trip safety:
:not([src^="data:"]) → (removed, 3 occurrences)
in apps/text/js/Wrapper-<hash>.chunk.mjs. It is wiped by every update, it fails the code
integrity check, and it allows SVG data URIs too — so it is a stopgap, not a fix.
Nextcloud Server version
34
Operating system
Debian/Ubuntu
PHP engine version
PHP 8.3
Web server
Nginx
Database engine version
MariaDB
Is this bug present after an update or on a fresh install?
Fresh Nextcloud Server install
Are you using the Nextcloud Server Encryption module?
None
What user-backends are you using?
- Default user-backend (database)
- LDAP/ Active Directory
- SSO - SAML
- Other
Configuration report
List of activated Apps
### Steps to reproduce
1. Create a Markdown file containing one inline image, e.g.
`` — a normal JPEG, nothing exotic.
2. Upload it to Nextcloud (WebDAV or the Files web UI). Confirm the file on the server still
contains the `data:` URI.
3. Open it in Files. Text displays the document — **the image is missing**, with no indication
that anything was omitted.
4. Change one word of the text and let Text save.
5. Download the file, or inspect it on the server.
### Expected behaviour
Either the image renders, or — at minimum — content Text cannot represent is **preserved on save**.
Silently discarding part of a user's file is the part that matters: the document was complete when
it was opened and is not when it is closed.
### Actual behaviour
The image is gone from the stored file.
Measured on a live instance (sizes and MD5s of the same document before and after):
| copy | size | md5 |
|---|---|---|
| as uploaded | 56344 | `ad73141d3179f82d35e97bf482fe23b7` |
| after opening in Text and editing one line | **2888** | `97f7f445021d9b82fbaa4ff015ae3757` |
The `` line is not altered or replaced — it is deleted. Nextcloud's
own file versioning kept the original, which is the only reason nothing was lost; a user without
versions enabled, or one who notices after the retention window, loses the images permanently.
### Where it comes from
Text builds image nodes from markdown-it tokens via the image node's `parseMarkdown`, and the
TipTap `Image` extension it uses declares:
addOptions() { return { inline: false, allowBase64: false, HTMLAttributes: {}, resize: false } }
…
parseHTML() { return [{ tag: this.options.allowBase64 ? "img[src]" : 'img[src]:not([src^="data:"])' }] }
`allowBase64` is TipTap's own default and Text never sets it, so `data:` sources are excluded. The
node is therefore never created; the document Text holds has no image in it; and the Markdown
serializer, which writes what the document contains, emits a file without the image.
Worth noting that every *other* layer already accepts these images:
- **markdown-it**, which Text bundles, explicitly permits them — its own validator is
`/^data:image\/(gif|png|jpeg|webp);/`, i.e. raster images pass and `data:image/svg+xml` does not.
- **Nextcloud's CSP** already allows `data:` images (`img-src * data: blob:`).
So the exclusion is not a considered security position — it is an unset option, inherited from a
library default, that happens to also delete user data.
### Suggested fix
1. **Never drop unrepresentable content on save.** Whatever is decided about rendering, a document
that arrives with content Text cannot display should not be written back without it. This is the
data-loss half and it is independent of the rest.
2. **Allow inert raster data URIs.** Enable base64 images for `data:image/png`, `image/jpeg`,
`image/gif`, `image/webp` and keep rejecting `data:image/svg+xml`, which is the only genuinely
dangerous case (SVG can carry script). markdown-it's existing allow-list is exactly this set, so
the two layers would finally agree.
3. If inline images are unwanted as a matter of product design, then **say so in the UI** — a
placeholder reading "1 image not displayed" would at least warn the user before they edit and
lose it.
### Workaround (for anyone finding this issue)
Removing the guard from the built chunk restores both rendering and round-trip safety:
:not([src^="data:"]) → (removed, 3 occurrences)
in `apps/text/js/Wrapper-<hash>.chunk.mjs`. It is wiped by every update, it fails the code
integrity check, and it allows SVG data URIs too — so it is a stopgap, not a fix.
### Server configuration
- Nextcloud: **34.0.3**
- Text app: **7.0.1**
- PHP: 8.3.6
- OS: Ubuntu 24.04 LTS
- Web server: nginx
- Database: MariaDB 10.11
### Client
Chromium-based browser, desktop. Not browser-specific — the content never reaches the DOM.
Nextcloud Signing status
Nextcloud Logs
Additional info
No response
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.
Research direction
Start by tracing the Text image node’s parseMarkdown path and the TipTap Image configuration described in the issue, rather than editing the built apps/text/js/Wrapper-.chunk.mjs workaround. Reproduce the Markdown round trip with a raster data URI and verify that editing preserves it while SVG data URIs remain rejected. Done means the image is rendered or explicitly represented, and saving no longer silently removes unsupported content.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, markdown
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 68/100