Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions apps/app/app/components/LegalDocument.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
</style>
7 changes: 3 additions & 4 deletions apps/app/shared/utils/legal-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. `img` is out: nothing lets an operator upload 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.
Expand All @@ -21,8 +21,7 @@ const parseMarkdown = createMarkdownParser({
breaks(),
security({
allowedTags: ALLOWED_TAGS,
allowedProtocols: ['http', 'https', 'mailto'],
allowDataImages: false
allowedProtocols: ['http', 'https', 'mailto']
})
]
})
Expand Down
6 changes: 6 additions & 0 deletions apps/app/tests/legal-markdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ describe('parseLegalMarkdown', () => {
expect(json(tree.nodes)).toContain('mailto:a@example.com')
})

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')
})

it('ignores component syntax instead of resolving a component', async () => {
const tree = await parseLegalMarkdown('::alert{type="info"}\nHello\n::')

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/2.self-hosting/5.legal-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.

Expand Down