Skip to content

Commit 593f107

Browse files
authored
fix(link): respect custom protocols #5468 (#5470)
When [we fixed a XSS vuln](#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
1 parent 6a0f4f3 commit 593f107

File tree

3 files changed

+49
-6
lines changed

3 files changed

+49
-6
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tiptap/extension-link": patch
3+
---
4+
5+
Respect custom protocols for links again, custom protocols are supported in additional to the default set #5468

packages/extension-link/src/link.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,24 @@ declare module '@tiptap/core' {
106106

107107
// From DOMPurify
108108
// https://github.com/cure53/DOMPurify/blob/main/src/regexp.js
109-
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex
110-
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
109+
// eslint-disable-next-line no-control-regex
110+
const ATTR_WHITESPACE = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g
111111

112-
function isAllowedUri(uri: string | undefined) {
113-
return !uri || uri.replace(ATTR_WHITESPACE, '').match(IS_ALLOWED_URI)
112+
function isAllowedUri(uri: string | undefined, protocols?: LinkOptions['protocols']) {
113+
const allowedProtocols: string[] = ['http', 'https', 'ftp', 'ftps', 'mailto', 'tel', 'callto', 'sms', 'cid', 'xmpp']
114+
115+
if (protocols) {
116+
protocols.forEach(protocol => {
117+
const nextProtocol = (typeof protocol === 'string' ? protocol : protocol.scheme)
118+
119+
if (nextProtocol) {
120+
allowedProtocols.push(nextProtocol)
121+
}
122+
})
123+
}
124+
125+
// eslint-disable-next-line no-useless-escape
126+
return !uri || uri.replace(ATTR_WHITESPACE, '').match(new RegExp(`^(?:(?:${allowedProtocols.join('|')}):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))`, 'i'))
114127
}
115128

116129
/**
@@ -187,7 +200,7 @@ export const Link = Mark.create<LinkOptions>({
187200
const href = (dom as HTMLElement).getAttribute('href')
188201

189202
// prevent XSS attacks
190-
if (!href || !isAllowedUri(href)) {
203+
if (!href || !isAllowedUri(href, this.options.protocols)) {
191204
return false
192205
}
193206
return null
@@ -197,7 +210,7 @@ export const Link = Mark.create<LinkOptions>({
197210

198211
renderHTML({ HTMLAttributes }) {
199212
// prevent XSS attacks
200-
if (!isAllowedUri(HTMLAttributes.href)) {
213+
if (!isAllowedUri(HTMLAttributes.href, this.options.protocols)) {
201214
// strip out the href
202215
return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]
203216
}

tests/cypress/integration/extensions/link.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,4 +250,29 @@ describe('extension-link', () => {
250250
getEditorEl()?.remove()
251251
})
252252
})
253+
254+
describe('custom protocols', () => {
255+
it('allows using additional custom protocols', () => {
256+
['custom://test.css', 'another-custom://protocol.html', ...validUrls].forEach(url => {
257+
editor = new Editor({
258+
element: createEditorEl(),
259+
extensions: [
260+
Document,
261+
Text,
262+
Paragraph,
263+
Link.configure({
264+
protocols: ['custom', { scheme: 'another-custom' }],
265+
}),
266+
],
267+
content: `<p><a href="${url}">hello world!</a></p>`,
268+
})
269+
270+
expect(editor.getHTML()).to.include(url)
271+
expect(JSON.stringify(editor.getJSON())).to.include(url)
272+
273+
editor?.destroy()
274+
getEditorEl()?.remove()
275+
})
276+
})
277+
})
253278
})

0 commit comments

Comments
 (0)