From b8728fafa052c097c06e876dc9407ae7534a4950 Mon Sep 17 00:00:00 2001 From: Thomas <28439359+thoda-dev@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:14:29 +0200 Subject: [PATCH 1/2] fix(legal): drop img from the allowed markdown tags --- apps/app/app/components/LegalDocument.vue | 5 ----- apps/app/shared/utils/legal-markdown.ts | 7 +++---- apps/app/tests/legal-markdown.test.ts | 6 ++++++ apps/docs/content/2.self-hosting/5.legal-pages.md | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/app/app/components/LegalDocument.vue b/apps/app/app/components/LegalDocument.vue index a99dd32..2f1fa30 100644 --- a/apps/app/app/components/LegalDocument.vue +++ b/apps/app/app/components/LegalDocument.vue @@ -152,9 +152,4 @@ defineProps<{ value: MarkdownDocumentType }>() border-top: 1px solid var(--ui-border); margin: 2rem 0; } - -.legal-document :deep(img) { - max-width: 100%; - border-radius: var(--ui-radius); -} diff --git a/apps/app/shared/utils/legal-markdown.ts b/apps/app/shared/utils/legal-markdown.ts index 3b9824e..fcc12d1 100644 --- a/apps/app/shared/utils/legal-markdown.ts +++ b/apps/app/shared/utils/legal-markdown.ts @@ -3,14 +3,14 @@ import breaks from 'comark/plugins/breaks' import security from 'comark/plugins/security' import type { MarkdownDocument } from 'comark' -// Everything plain markdown can produce, and nothing else. +// Everything plain markdown can produce. No `img`: an instance has nowhere to host one. const ALLOWED_TAGS = [ 'p', 'br', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 's', 'del', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'th', 'td', - 'a', 'img' + 'a' ] // `registerDefaultPlugins: false` is the load-bearing line: it turns off raw HTML, component syntax and free-form attributes, any of which would put script on a page that holds decryption keys. @@ -21,8 +21,7 @@ const parseMarkdown = createMarkdownParser({ breaks(), security({ allowedTags: ALLOWED_TAGS, - allowedProtocols: ['http', 'https', 'mailto'], - allowDataImages: false + allowedProtocols: ['http', 'https', 'mailto'] }) ] }) diff --git a/apps/app/tests/legal-markdown.test.ts b/apps/app/tests/legal-markdown.test.ts index 30f5927..50d3172 100644 --- a/apps/app/tests/legal-markdown.test.ts +++ b/apps/app/tests/legal-markdown.test.ts @@ -61,6 +61,12 @@ describe('parseLegalMarkdown', () => { expect(json(tree.nodes)).toContain('mailto:a@example.com') }) + it('drops an image, which the instance could never serve anyway', async () => { + const tree = await parseLegalMarkdown('![logo](/logo.png) and ![remote](https://example.com/logo.png)') + + expect(tags(tree.nodes)).not.toContain('img') + }) + it('ignores component syntax instead of resolving a component', async () => { const tree = await parseLegalMarkdown('::alert{type="info"}\nHello\n::') diff --git a/apps/docs/content/2.self-hosting/5.legal-pages.md b/apps/docs/content/2.self-hosting/5.legal-pages.md index 13a703f..b23e103 100644 --- a/apps/docs/content/2.self-hosting/5.legal-pages.md +++ b/apps/docs/content/2.self-hosting/5.legal-pages.md @@ -51,7 +51,7 @@ Three of the retention fields arrive already filled from your own configuration: ## What Markdown is allowed -Headings, paragraphs, bold and italic, lists, links, images, quotes, code and tables. That is the whole list. A single newline is kept as a line break, the way it works in a GitHub comment, so an address block stays on several lines without any markup. +Headings, paragraphs, bold and italic, lists, links, quotes, code and tables. That is the whole list; images are not on it, since an instance has nowhere to host one and the CSP would refuse to load it from anywhere else. A single newline is kept as a line break, the way it works in a GitHub comment, so an address block stays on several lines without any markup. Raw HTML and component syntax are **switched off, deliberately**. In an app where the browser holds the only decryption key, a script tag on any page is not a defaced page, it is a leaked key — so the parser never produces one, and link URLs are restricted to `http`, `https` and `mailto`. If you paste HTML into the editor it appears on the page as the characters you typed. From ee8253b58217f79a997ecc85ca3850ff11af2fdc Mon Sep 17 00:00:00 2001 From: Thomas <28439359+thoda-dev@users.noreply.github.com> Date: Mon, 31 Aug 2026 17:21:46 +0200 Subject: [PATCH 2/2] docs(legal): say precisely why images are not allowed --- apps/app/shared/utils/legal-markdown.ts | 2 +- apps/app/tests/legal-markdown.test.ts | 2 +- apps/docs/content/2.self-hosting/5.legal-pages.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/app/shared/utils/legal-markdown.ts b/apps/app/shared/utils/legal-markdown.ts index fcc12d1..e29bd8b 100644 --- a/apps/app/shared/utils/legal-markdown.ts +++ b/apps/app/shared/utils/legal-markdown.ts @@ -3,7 +3,7 @@ import breaks from 'comark/plugins/breaks' import security from 'comark/plugins/security' import type { MarkdownDocument } from 'comark' -// Everything plain markdown can produce. No `img`: an instance has nowhere to host one. +// Everything plain markdown can produce. `img` is out: nothing lets an operator upload one. const ALLOWED_TAGS = [ 'p', 'br', 'hr', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', diff --git a/apps/app/tests/legal-markdown.test.ts b/apps/app/tests/legal-markdown.test.ts index 50d3172..979e35d 100644 --- a/apps/app/tests/legal-markdown.test.ts +++ b/apps/app/tests/legal-markdown.test.ts @@ -61,7 +61,7 @@ describe('parseLegalMarkdown', () => { expect(json(tree.nodes)).toContain('mailto:a@example.com') }) - it('drops an image, which the instance could never serve anyway', async () => { + it('drops an image, relative or remote', async () => { const tree = await parseLegalMarkdown('![logo](/logo.png) and ![remote](https://example.com/logo.png)') expect(tags(tree.nodes)).not.toContain('img') diff --git a/apps/docs/content/2.self-hosting/5.legal-pages.md b/apps/docs/content/2.self-hosting/5.legal-pages.md index b23e103..e7ac9be 100644 --- a/apps/docs/content/2.self-hosting/5.legal-pages.md +++ b/apps/docs/content/2.self-hosting/5.legal-pages.md @@ -51,7 +51,7 @@ Three of the retention fields arrive already filled from your own configuration: ## What Markdown is allowed -Headings, paragraphs, bold and italic, lists, links, quotes, code and tables. That is the whole list; images are not on it, since an instance has nowhere to host one and the CSP would refuse to load it from anywhere else. A single newline is kept as a line break, the way it works in a GitHub comment, so an address block stays on several lines without any markup. +Headings, paragraphs, bold and italic, lists, links, quotes, code and tables. That is the whole list; images are not on it, since nothing lets an operator upload one and the CSP loads images from the instance's own origin only. A single newline is kept as a line break, the way it works in a GitHub comment, so an address block stays on several lines without any markup. Raw HTML and component syntax are **switched off, deliberately**. In an app where the browser holds the only decryption key, a script tag on any page is not a defaced page, it is a leaked key — so the parser never produces one, and link URLs are restricted to `http`, `https` and `mailto`. If you paste HTML into the editor it appears on the page as the characters you typed.