Skip to content

Commit 973a495

Browse files
committed
Fix Broken Hyperlinks in Markdown to HTML conversion.
Thanks to xet7 ! Fixes #5932
1 parent e1902d5 commit 973a495

File tree

4 files changed

+113
-103
lines changed

4 files changed

+113
-103
lines changed

client/lib/secureDOMPurify.js

Lines changed: 55 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,25 @@ import DOMPurify from 'dompurify';
33
// Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
44
export function getSecureDOMPurifyConfig() {
55
return {
6-
// Block dangerous elements that can cause XSS and CSS injection
7-
FORBID_TAGS: [
8-
'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath',
9-
'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform',
10-
'animateMotion', 'set', 'switch', 'foreignObject', 'script', 'style', 'link',
11-
'meta', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'textarea',
12-
'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset',
13-
'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'
14-
],
15-
// Block dangerous attributes that can cause XSS and CSS injection
16-
FORBID_ATTR: [
17-
'xlink:href', 'href', 'onload', 'onerror', 'onclick', 'onmouseover',
18-
'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect',
19-
'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress',
20-
'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove',
21-
'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend',
22-
'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough',
23-
'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata',
24-
'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying',
25-
'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled',
26-
'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload',
27-
'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage',
28-
'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'
29-
],
30-
// Allow only safe image formats and protocols
6+
// Allow common markdown elements including anchor tags
7+
ALLOWED_TAGS: ['a', 'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'div', 'span'],
8+
// Allow safe attributes including href for anchor tags
9+
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'width', 'height', 'target', 'rel'],
10+
// Allow safe protocols for links
3111
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
32-
// Remove dangerous protocols
12+
// Allow unknown protocols but be cautious
3313
ALLOW_UNKNOWN_PROTOCOLS: false,
34-
// Sanitize URLs to prevent malicious content loading
14+
// Sanitize DOM for security
3515
SANITIZE_DOM: true,
36-
// Remove dangerous elements completely
37-
KEEP_CONTENT: false,
38-
// Additional security measures
39-
ADD_ATTR: [],
16+
// Keep content but sanitize it
17+
KEEP_CONTENT: true,
18+
// Block dangerous elements that can cause XSS
19+
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'applet', 'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath', 'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform', 'animateMotion', 'set', 'switch', 'foreignObject', 'link', 'meta', 'form', 'input', 'textarea', 'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset', 'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'],
20+
// Block dangerous attributes but allow safe href
21+
FORBID_ATTR: ['xlink:href', 'onload', 'onerror', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect', 'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress', 'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove', 'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled', 'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload', 'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage', 'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'],
4022
// Block data URIs that could contain malicious content
4123
ALLOW_DATA_ATTR: false,
42-
// Custom hook to further sanitize content
24+
// Custom hooks for additional security
4325
HOOKS: {
4426
uponSanitizeElement: function(node, data) {
4527
// Block any remaining dangerous elements
@@ -51,14 +33,37 @@ export function getSecureDOMPurifyConfig() {
5133
return false;
5234
}
5335

54-
// Block img tags with SVG data URIs
36+
// Block img tags with SVG data URIs that could contain malicious JavaScript
5537
if (node.tagName && node.tagName.toLowerCase() === 'img') {
5638
const src = node.getAttribute('src');
57-
if (src && (src.startsWith('data:image/svg') || src.endsWith('.svg'))) {
58-
if (process.env.DEBUG === 'true') {
59-
console.warn('Blocked potentially malicious SVG image:', src);
39+
if (src) {
40+
// Block all SVG data URIs to prevent XSS via embedded JavaScript
41+
if (src.startsWith('data:image/svg') || src.endsWith('.svg')) {
42+
if (process.env.DEBUG === 'true') {
43+
console.warn('Blocked potentially malicious SVG image:', src);
44+
}
45+
return false;
46+
}
47+
48+
// Additional check for base64 encoded SVG with script tags
49+
if (src.startsWith('data:image/svg+xml;base64,')) {
50+
try {
51+
const base64Content = src.split(',')[1];
52+
const decodedContent = atob(base64Content);
53+
if (decodedContent.includes('<script') || decodedContent.includes('javascript:')) {
54+
if (process.env.DEBUG === 'true') {
55+
console.warn('Blocked SVG with embedded JavaScript:', src.substring(0, 100) + '...');
56+
}
57+
return false;
58+
}
59+
} catch (e) {
60+
// If decoding fails, block it as a safety measure
61+
if (process.env.DEBUG === 'true') {
62+
console.warn('Blocked malformed SVG data URI:', src);
63+
}
64+
return false;
65+
}
6066
}
61-
return false;
6267
}
6368
}
6469

@@ -100,6 +105,19 @@ export function getSecureDOMPurifyConfig() {
100105
return false;
101106
}
102107

108+
// Allow href attribute for anchor tags only
109+
if (data.attrName === 'href') {
110+
// Only allow href on anchor tags
111+
if (node.tagName && node.tagName.toLowerCase() === 'a') {
112+
return true;
113+
} else {
114+
if (process.env.DEBUG === 'true') {
115+
console.warn('Blocked href attribute on non-anchor element:', node.tagName);
116+
}
117+
return false;
118+
}
119+
}
120+
103121
return true;
104122
}
105123
}

imports/lib/secureDOMPurify.js

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,25 @@ import DOMPurify from 'dompurify';
33
// Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
44
export function getSecureDOMPurifyConfig() {
55
return {
6-
// Block dangerous elements that can cause XSS and CSS injection
7-
FORBID_TAGS: [
8-
'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath',
9-
'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform',
10-
'animateMotion', 'set', 'switch', 'foreignObject', 'script', 'style', 'link',
11-
'meta', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'textarea',
12-
'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset',
13-
'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'
14-
],
15-
// Block dangerous attributes that can cause XSS and CSS injection
16-
FORBID_ATTR: [
17-
'xlink:href', 'href', 'onload', 'onerror', 'onclick', 'onmouseover',
18-
'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect',
19-
'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress',
20-
'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove',
21-
'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend',
22-
'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough',
23-
'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata',
24-
'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying',
25-
'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled',
26-
'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload',
27-
'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage',
28-
'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'
29-
],
30-
// Allow only safe image formats and protocols
6+
// Allow common markdown elements including anchor tags
7+
ALLOWED_TAGS: ['a', 'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'div', 'span'],
8+
// Allow safe attributes including href for anchor tags
9+
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'width', 'height', 'target', 'rel'],
10+
// Allow safe protocols for links
3111
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
32-
// Remove dangerous protocols
12+
// Allow unknown protocols but be cautious
3313
ALLOW_UNKNOWN_PROTOCOLS: false,
34-
// Sanitize URLs to prevent malicious content loading
14+
// Sanitize DOM for security
3515
SANITIZE_DOM: true,
36-
// Remove dangerous elements completely
37-
KEEP_CONTENT: false,
38-
// Additional security measures
39-
ADD_ATTR: [],
16+
// Keep content but sanitize it
17+
KEEP_CONTENT: true,
18+
// Block dangerous elements that can cause XSS
19+
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'applet', 'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath', 'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform', 'animateMotion', 'set', 'switch', 'foreignObject', 'link', 'meta', 'form', 'input', 'textarea', 'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset', 'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'],
20+
// Block dangerous attributes but allow safe href
21+
FORBID_ATTR: ['xlink:href', 'onload', 'onerror', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect', 'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress', 'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove', 'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled', 'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload', 'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage', 'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'],
4022
// Block data URIs that could contain malicious content
4123
ALLOW_DATA_ATTR: false,
42-
// Custom hook to further sanitize content
24+
// Custom hooks for additional security
4325
HOOKS: {
4426
uponSanitizeElement: function(node, data) {
4527
// Block any remaining dangerous elements
@@ -123,6 +105,19 @@ export function getSecureDOMPurifyConfig() {
123105
return false;
124106
}
125107

108+
// Allow href attribute for anchor tags only
109+
if (data.attrName === 'href') {
110+
// Only allow href on anchor tags
111+
if (node.tagName && node.tagName.toLowerCase() === 'a') {
112+
return true;
113+
} else {
114+
if (process.env.DEBUG === 'true') {
115+
console.warn('Blocked href attribute on non-anchor element:', node.tagName);
116+
}
117+
return false;
118+
}
119+
}
120+
126121
return true;
127122
}
128123
}

packages/markdown/src/secureDOMPurify.js

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,43 +3,25 @@ import DOMPurify from 'dompurify';
33
// Centralized secure DOMPurify configuration to prevent XSS and CSS injection attacks
44
export function getSecureDOMPurifyConfig() {
55
return {
6-
// Block dangerous elements that can cause XSS and CSS injection
7-
FORBID_TAGS: [
8-
'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath',
9-
'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform',
10-
'animateMotion', 'set', 'switch', 'foreignObject', 'script', 'style', 'link',
11-
'meta', 'iframe', 'object', 'embed', 'applet', 'form', 'input', 'textarea',
12-
'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset',
13-
'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'
14-
],
15-
// Block dangerous attributes that can cause XSS and CSS injection
16-
FORBID_ATTR: [
17-
'xlink:href', 'onload', 'onerror', 'onclick', 'onmouseover',
18-
'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect',
19-
'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress',
20-
'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove',
21-
'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend',
22-
'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough',
23-
'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata',
24-
'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying',
25-
'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled',
26-
'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload',
27-
'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage',
28-
'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'
29-
],
30-
// Allow only safe image formats and protocols
6+
// Allow common markdown elements including anchor tags
7+
ALLOWED_TAGS: ['a', 'p', 'br', 'strong', 'em', 'u', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr', 'div', 'span'],
8+
// Allow safe attributes including href for anchor tags
9+
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'width', 'height', 'target', 'rel'],
10+
// Allow safe protocols for links
3111
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
32-
// Remove dangerous protocols
12+
// Allow unknown protocols but be cautious
3313
ALLOW_UNKNOWN_PROTOCOLS: false,
34-
// Sanitize URLs to prevent malicious content loading
14+
// Sanitize DOM for security
3515
SANITIZE_DOM: true,
36-
// Remove dangerous elements completely
37-
KEEP_CONTENT: false,
38-
// Additional security measures
39-
ADD_ATTR: [],
16+
// Keep content but sanitize it
17+
KEEP_CONTENT: true,
18+
// Block dangerous elements that can cause XSS
19+
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'applet', 'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath', 'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform', 'animateMotion', 'set', 'switch', 'foreignObject', 'link', 'meta', 'form', 'input', 'textarea', 'select', 'option', 'button', 'label', 'fieldset', 'legend', 'frameset', 'frame', 'noframes', 'base', 'basefont', 'isindex', 'dir', 'menu', 'menuitem'],
20+
// Block dangerous attributes but allow safe href
21+
FORBID_ATTR: ['xlink:href', 'onload', 'onerror', 'onclick', 'onmouseover', 'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect', 'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress', 'onmousedown', 'onmouseup', 'onmouseover', 'onmouseout', 'onmousemove', 'ondblclick', 'oncontextmenu', 'onwheel', 'ontouchstart', 'ontouchend', 'ontouchmove', 'ontouchcancel', 'onabort', 'oncanplay', 'oncanplaythrough', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onpause', 'onplay', 'onplaying', 'onprogress', 'onratechange', 'onseeked', 'onseeking', 'onstalled', 'onsuspend', 'ontimeupdate', 'onvolumechange', 'onwaiting', 'onbeforeunload', 'onhashchange', 'onpagehide', 'onpageshow', 'onpopstate', 'onstorage', 'onunload', 'style', 'class', 'id', 'data-*', 'aria-*'],
4022
// Block data URIs that could contain malicious content
4123
ALLOW_DATA_ATTR: false,
42-
// Custom hook to further sanitize content
24+
// Custom hooks for additional security
4325
HOOKS: {
4426
uponSanitizeElement: function(node, data) {
4527
// Block any remaining dangerous elements
@@ -123,6 +105,19 @@ export function getSecureDOMPurifyConfig() {
123105
return false;
124106
}
125107

108+
// Allow href attribute for anchor tags only
109+
if (data.attrName === 'href') {
110+
// Only allow href on anchor tags
111+
if (node.tagName && node.tagName.toLowerCase() === 'a') {
112+
return true;
113+
} else {
114+
if (process.env.DEBUG === 'true') {
115+
console.warn('Blocked href attribute on non-anchor element:', node.tagName);
116+
}
117+
return false;
118+
}
119+
}
120+
126121
return true;
127122
}
128123
}

packages/markdown/src/template-integration.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ if (Package.ui) {
214214
if (self.templateContentBlock) {
215215
text = Blaze._toText(self.templateContentBlock, HTML.TEXTMODE.STRING);
216216
}
217-
if (text.includes("[]") !== false) {
217+
if (text.includes("[]")) {
218218
// Prevent hiding info: https://wekan.github.io/hall-of-fame/invisiblebleed/
219219
// If markdown link does not have description, do not render markdown, instead show all of markdown source code using preformatted text.
220220
// Also show html comments.
@@ -223,7 +223,9 @@ if (Package.ui) {
223223
// Prevent hiding info: https://wekan.github.io/hall-of-fame/invisiblebleed/
224224
// If text does not have hidden markdown link, render all markdown.
225225
// Also show html comments.
226-
return HTML.Raw(DOMPurify.sanitize(Markdown.render(text).replace('<!--', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!">&lt;!--</font>').replace('-->', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!">--&gt;</font>'), getSecureDOMPurifyConfig()));
226+
const renderedMarkdown = Markdown.render(text).replace('<!--', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!">&lt;!--</font>').replace('-->', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!">--&gt;</font>');
227+
const sanitized = DOMPurify.sanitize(renderedMarkdown, getSecureDOMPurifyConfig());
228+
return HTML.Raw(sanitized);
227229
}
228230
}));
229231
}

0 commit comments

Comments
 (0)