mirror of
https://github.com/ueberdosis/tiptap.git
synced 2024-11-27 14:59:27 +08:00
fix(extension-link): use whitelist for allowed href values
This commit is contained in:
parent
ef7d195311
commit
980b54f62b
@ -95,6 +95,15 @@ 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
|
||||
|
||||
function isAllowedUri(uri: string | undefined) {
|
||||
return !uri || uri.replace(ATTR_WHITESPACE, '').match(IS_ALLOWED_URI)
|
||||
}
|
||||
|
||||
/**
|
||||
* This extension allows you to create links.
|
||||
* @see https://www.tiptap.dev/api/marks/link
|
||||
@ -161,12 +170,11 @@ export const Link = Mark.create<LinkOptions>({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
// False positive; we're explicitly checking for javascript: links to ignore them
|
||||
// eslint-disable-next-line no-script-url
|
||||
if (HTMLAttributes.href?.startsWith('javascript:')) {
|
||||
if (!isAllowedUri(HTMLAttributes.href)) {
|
||||
// strip out the href
|
||||
return ['a', mergeAttributes(this.options.HTMLAttributes, { ...HTMLAttributes, href: '' }), 0]
|
||||
}
|
||||
|
||||
return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0]
|
||||
},
|
||||
|
||||
|
@ -20,45 +20,188 @@ describe('extension-link', () => {
|
||||
}
|
||||
const getEditorEl = () => document.querySelector(`.${editorElClass}`)
|
||||
|
||||
it('does not output src tag for javascript schema', () => {
|
||||
editor = new Editor({
|
||||
element: createEditorEl(),
|
||||
extensions: [
|
||||
Document,
|
||||
Text,
|
||||
Paragraph,
|
||||
Link,
|
||||
],
|
||||
content: {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'hello world!',
|
||||
marks: [
|
||||
{
|
||||
type: 'link',
|
||||
attrs: {
|
||||
// We have to disable the eslint rule here because we're trying to purposely test eval urls
|
||||
// eslint-disable-next-line no-script-url
|
||||
href: 'javascript:alert(window.origin)',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
const validUrls = [
|
||||
'https://example.com',
|
||||
'http://example.com',
|
||||
'/same-site/index.html',
|
||||
'../relative.html',
|
||||
'mailto:info@example.com',
|
||||
'ftp://info@example.com',
|
||||
]
|
||||
|
||||
validUrls.forEach(url => {
|
||||
it('does output href tag for valid schemas', () => {
|
||||
editor = new Editor({
|
||||
element: createEditorEl(),
|
||||
extensions: [
|
||||
Document,
|
||||
Text,
|
||||
Paragraph,
|
||||
Link,
|
||||
],
|
||||
},
|
||||
content: {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'hello world!',
|
||||
marks: [
|
||||
{
|
||||
type: 'link',
|
||||
attrs: {
|
||||
href: url,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getHTML()).to.include(url)
|
||||
|
||||
editor?.destroy()
|
||||
getEditorEl()?.remove()
|
||||
})
|
||||
})
|
||||
|
||||
// We have to disable the eslint rule here because we're trying to purposely test eval urls
|
||||
// Examples inspired by: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet#protocols
|
||||
const invalidUrls = [
|
||||
// A standard JavaScript protocol
|
||||
// eslint-disable-next-line no-script-url
|
||||
expect(editor.getHTML()).to.not.include('javascript:alert(window.origin)')
|
||||
'javascript:alert(window.origin)',
|
||||
|
||||
editor?.destroy()
|
||||
getEditorEl()?.remove()
|
||||
// The protocol is not case sensitive
|
||||
// eslint-disable-next-line no-script-url
|
||||
'jAvAsCrIpT:alert(window.origin)',
|
||||
|
||||
// Characters \x01-\x20 are allowed before the protocol
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x00javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x01javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x02javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x03javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x04javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x05javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x06javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x07javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x08javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x09javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x0ajavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x0bjavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x0cjavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x0djavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x0ejavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x0fjavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x10javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x11javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x12javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x13javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x14javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x15javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x16javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x17javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x18javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x19javascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x1ajavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x1bjavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x1cjavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x1djavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x1ejavascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'\x1fjavascript:alert(window.origin)',
|
||||
|
||||
// Characters \x09,\x0a,\x0d are allowed inside the protocol
|
||||
// eslint-disable-next-line no-script-url
|
||||
'java\x09script:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'java\x0ascript:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'java\x0dscript:alert(window.origin)',
|
||||
|
||||
// Characters \x09,\x0a,\x0d are allowed after protocol name before the colon
|
||||
// eslint-disable-next-line no-script-url
|
||||
'javascript\x09:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'javascript\x0a:alert(window.origin)',
|
||||
// eslint-disable-next-line no-script-url
|
||||
'javascript\x0d:alert(window.origin)',
|
||||
]
|
||||
|
||||
invalidUrls.forEach(url => {
|
||||
it('does not output src tag for javascript schema', () => {
|
||||
editor = new Editor({
|
||||
element: createEditorEl(),
|
||||
extensions: [
|
||||
Document,
|
||||
Text,
|
||||
Paragraph,
|
||||
Link,
|
||||
],
|
||||
content: {
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: 'hello world!',
|
||||
marks: [
|
||||
{
|
||||
type: 'link',
|
||||
attrs: {
|
||||
href: url,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(editor.getHTML()).to.not.include(url)
|
||||
|
||||
editor?.destroy()
|
||||
getEditorEl()?.remove()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user