fix(link): respect custom protocols #5468 (#5470)

When [we fixed a XSS vuln](https://github.com/ueberdosis/tiptap/pull/5160), we inadvertently broke the ability to use custom protocols, this resolves that by allowing additional custom protocols to be considered valid and not stripped out
This commit is contained in:
Nick Perez 2024-08-15 08:57:59 +02:00 committed by GitHub
parent 6a0f4f30f8
commit 593f1070a8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 49 additions and 6 deletions

View File

@ -0,0 +1,5 @@
---
"@tiptap/extension-link": patch
---
Respect custom protocols for links again, custom protocols are supported in additional to the default set #5468

View File

@ -106,11 +106,24 @@ declare module '@tiptap/core' {
// From DOMPurify
// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
const IS_ALLOWED_URI = /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape
// eslint-disable-next-line no-control-regex
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g
function isAllowedUri(uri: string | undefined) {
return !uri || uri.replace(ATTR_WHITESPACE, '').match(IS_ALLOWED_URI)
function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {
const allowedProtocols: string[] = ['http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid', 'xmpp']
if (protocols) {
protocols.forEach(protocol => {
const nextProtocol = (typeof protocol === 'string' ? protocol : protocol.scheme)
if (nextProtocol) {
allowedProtocols.push(nextProtocol)
}
})
}
// eslint-disable-next-line no-useless-escape
return !uri || uri.replace(ATTR_WHITESPACE, '').match(new RegExp(`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`, 'i'))
}
/**
@ -187,7 +200,7 @@ export const Link = Mark.create<LinkOptions>({
const href = (dom as HTMLElement).getAttribute('href')
// prevent XSS attacks
if (!href || !isAllowedUri(href)) {
if (!href || !isAllowedUri(href, this.options.protocols)) {
return false
}
return null
@ -197,7 +210,7 @@ export const Link = Mark.create<LinkOptions>({
renderHTML({ HTMLAttributes }) {
// prevent XSS attacks
if (!isAllowedUri(HTMLAttributes.href)) {
if (!isAllowedUri(HTMLAttributes.href, this.options.protocols)) {
// strip out the href
return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]
}

View File

@ -250,4 +250,29 @@ describe('extension-link', () => {
getEditorEl()?.remove()
})
})
describe('custom protocols', () => {
it('allows using additional custom protocols', () => {
['custom://test.css', 'another-custom://protocol.html', ...validUrls].forEach(url => {
editor = new Editor({
element: createEditorEl(),
extensions: [
Document,
Text,
Paragraph,
Link.configure({
protocols: ['custom', { scheme: 'another-custom' }],
}),
],
content: `<p><a href="${url}">hello world!</a></p>`,
})
expect(editor.getHTML()).to.include(url)
expect(JSON.stringify(editor.getJSON())).to.include(url)
editor?.destroy()
getEditorEl()?.remove()
})
})
})
})