mirror of
https://github.com/wekan/wekan.git
synced 2025-12-16 23:40:13 +01:00
Security Fix FG-VD-22-078: Prevent SVG Billion Laughs Attack.
Thanks to Nguyen Thanh Nguyen of Fortinet's FortiGuard Labs and xet7 !
This commit is contained in:
parent
5bc5171220
commit
30c1597b65
2 changed files with 208 additions and 2 deletions
|
|
@ -148,6 +148,43 @@ if (Meteor.isServer) {
|
||||||
});
|
});
|
||||||
|
|
||||||
Meteor.methods({
|
Meteor.methods({
|
||||||
|
// Validate image URL to prevent SVG-based DoS attacks
|
||||||
|
validateImageUrl(imageUrl) {
|
||||||
|
check(imageUrl, String);
|
||||||
|
|
||||||
|
if (!imageUrl) {
|
||||||
|
return { valid: false, reason: 'Empty URL' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block SVG files and data URIs
|
||||||
|
if (imageUrl.endsWith('.svg') || imageUrl.startsWith('data:image/svg')) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Blocked potentially malicious SVG image URL:', imageUrl);
|
||||||
|
}
|
||||||
|
return { valid: false, reason: 'SVG images are blocked for security reasons' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block data URIs that could contain malicious content
|
||||||
|
if (imageUrl.startsWith('data:')) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Blocked data URI image URL:', imageUrl);
|
||||||
|
}
|
||||||
|
return { valid: false, reason: 'Data URIs are blocked for security reasons' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate URL format
|
||||||
|
try {
|
||||||
|
const url = new URL(imageUrl);
|
||||||
|
// Only allow http and https protocols
|
||||||
|
if (!['http:', 'https:'].includes(url.protocol)) {
|
||||||
|
return { valid: false, reason: 'Only HTTP and HTTPS protocols are allowed' };
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return { valid: false, reason: 'Invalid URL format' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
},
|
||||||
moveAttachmentToStorage(fileObjId, storageDestination) {
|
moveAttachmentToStorage(fileObjId, storageDestination) {
|
||||||
check(fileObjId, String);
|
check(fileObjId, String);
|
||||||
check(storageDestination, String);
|
check(storageDestination, String);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,55 @@
|
||||||
import DOMPurify from 'dompurify';
|
import DOMPurify from 'dompurify';
|
||||||
|
|
||||||
|
// Secure DOMPurify configuration to prevent SVG-based DoS attacks
|
||||||
|
function getSecureDOMPurifyConfig() {
|
||||||
|
return {
|
||||||
|
// Block dangerous SVG elements that can cause exponential expansion
|
||||||
|
FORBID_TAGS: [
|
||||||
|
'svg', 'defs', 'use', 'g', 'symbol', 'marker', 'pattern', 'mask', 'clipPath',
|
||||||
|
'linearGradient', 'radialGradient', 'stop', 'animate', 'animateTransform',
|
||||||
|
'animateMotion', 'set', 'switch', 'foreignObject', 'script', 'style'
|
||||||
|
],
|
||||||
|
// Block dangerous SVG attributes
|
||||||
|
FORBID_ATTR: [
|
||||||
|
'xlink:href', 'href', 'onload', 'onerror', 'onclick', 'onmouseover',
|
||||||
|
'onfocus', 'onblur', 'onchange', 'onsubmit', 'onreset', 'onselect',
|
||||||
|
'onunload', 'onresize', 'onscroll', 'onkeydown', 'onkeyup', 'onkeypress'
|
||||||
|
],
|
||||||
|
// Allow only safe image formats
|
||||||
|
ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i,
|
||||||
|
// Remove dangerous protocols
|
||||||
|
ALLOW_UNKNOWN_PROTOCOLS: false,
|
||||||
|
// Sanitize URLs to prevent malicious content loading
|
||||||
|
SANITIZE_DOM: true,
|
||||||
|
// Remove dangerous elements completely
|
||||||
|
KEEP_CONTENT: false,
|
||||||
|
// Additional security measures
|
||||||
|
ADD_ATTR: [],
|
||||||
|
// Block data URIs that could contain malicious SVG
|
||||||
|
ALLOW_DATA_ATTR: false,
|
||||||
|
// Custom hook to further sanitize content
|
||||||
|
HOOKS: {
|
||||||
|
uponSanitizeElement: function(node, data) {
|
||||||
|
// Block any remaining SVG elements
|
||||||
|
if (node.tagName && node.tagName.toLowerCase() === 'svg') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Block img tags with SVG data URIs
|
||||||
|
if (node.tagName && node.tagName.toLowerCase() === 'img') {
|
||||||
|
const src = node.getAttribute('src');
|
||||||
|
if (src && (src.startsWith('data:image/svg') || src.endsWith('.svg'))) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Blocked potentially malicious SVG image:', src);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var Markdown = require('markdown-it')({
|
var Markdown = require('markdown-it')({
|
||||||
html: true,
|
html: true,
|
||||||
linkify: true,
|
linkify: true,
|
||||||
|
|
@ -41,6 +91,125 @@ Markdown.use(emoji);
|
||||||
var mathjax = require('markdown-it-mathjax3');
|
var mathjax = require('markdown-it-mathjax3');
|
||||||
Markdown.use(mathjax);
|
Markdown.use(mathjax);
|
||||||
|
|
||||||
|
// Custom plugin to prevent SVG-based DoS attacks
|
||||||
|
Markdown.use(function(md) {
|
||||||
|
// Filter out dangerous SVG content in markdown
|
||||||
|
md.core.ruler.push('svg-dos-protection', function(state) {
|
||||||
|
const tokens = state.tokens;
|
||||||
|
|
||||||
|
for (let i = 0; i < tokens.length; i++) {
|
||||||
|
const token = tokens[i];
|
||||||
|
|
||||||
|
// Check for image tokens that might contain SVG
|
||||||
|
if (token.type === 'image') {
|
||||||
|
const src = token.attrGet('src');
|
||||||
|
if (src) {
|
||||||
|
// Block SVG data URIs and .svg files
|
||||||
|
if (src.startsWith('data:image/svg') || src.endsWith('.svg')) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Blocked potentially malicious SVG image in markdown:', src);
|
||||||
|
}
|
||||||
|
// Replace with a warning message
|
||||||
|
token.type = 'paragraph_open';
|
||||||
|
token.tag = 'p';
|
||||||
|
token.nesting = 1;
|
||||||
|
token.attrSet('style', 'color: red; background: #ffe6e6; padding: 8px; border: 1px solid #ff9999;');
|
||||||
|
token.attrSet('title', 'Blocked potentially malicious SVG image');
|
||||||
|
|
||||||
|
// Add warning text token
|
||||||
|
const warningToken = {
|
||||||
|
type: 'text',
|
||||||
|
content: '⚠️ Blocked potentially malicious SVG image for security reasons',
|
||||||
|
level: token.level,
|
||||||
|
markup: '',
|
||||||
|
info: '',
|
||||||
|
meta: null,
|
||||||
|
block: true,
|
||||||
|
hidden: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert warning token after the paragraph open
|
||||||
|
tokens.splice(i + 1, 0, warningToken);
|
||||||
|
|
||||||
|
// Add paragraph close token
|
||||||
|
const closeToken = {
|
||||||
|
type: 'paragraph_close',
|
||||||
|
tag: 'p',
|
||||||
|
nesting: -1,
|
||||||
|
level: token.level,
|
||||||
|
markup: '',
|
||||||
|
info: '',
|
||||||
|
meta: null,
|
||||||
|
block: true,
|
||||||
|
hidden: false
|
||||||
|
};
|
||||||
|
tokens.splice(i + 2, 0, closeToken);
|
||||||
|
|
||||||
|
// Remove the original image token
|
||||||
|
tokens.splice(i, 1);
|
||||||
|
i--; // Adjust index since we removed a token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for HTML tokens that might contain SVG
|
||||||
|
if (token.type === 'html_block' || token.type === 'html_inline') {
|
||||||
|
const content = token.content;
|
||||||
|
if (content && (
|
||||||
|
content.includes('<svg') ||
|
||||||
|
content.includes('data:image/svg') ||
|
||||||
|
content.includes('xlink:href') ||
|
||||||
|
content.includes('<use') ||
|
||||||
|
content.includes('<defs>')
|
||||||
|
)) {
|
||||||
|
if (process.env.DEBUG === 'true') {
|
||||||
|
console.warn('Blocked potentially malicious SVG content in HTML:', content.substring(0, 100) + '...');
|
||||||
|
}
|
||||||
|
// Replace with warning
|
||||||
|
token.type = 'paragraph_open';
|
||||||
|
token.tag = 'p';
|
||||||
|
token.nesting = 1;
|
||||||
|
token.attrSet('style', 'color: red; background: #ffe6e6; padding: 8px; border: 1px solid #ff9999;');
|
||||||
|
token.attrSet('title', 'Blocked potentially malicious SVG content');
|
||||||
|
|
||||||
|
// Add warning text
|
||||||
|
const warningToken = {
|
||||||
|
type: 'text',
|
||||||
|
content: '⚠️ Blocked potentially malicious SVG content for security reasons',
|
||||||
|
level: token.level,
|
||||||
|
markup: '',
|
||||||
|
info: '',
|
||||||
|
meta: null,
|
||||||
|
block: true,
|
||||||
|
hidden: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insert warning token after the paragraph open
|
||||||
|
tokens.splice(i + 1, 0, warningToken);
|
||||||
|
|
||||||
|
// Add paragraph close token
|
||||||
|
const closeToken = {
|
||||||
|
type: 'paragraph_close',
|
||||||
|
tag: 'p',
|
||||||
|
nesting: -1,
|
||||||
|
level: token.level,
|
||||||
|
markup: '',
|
||||||
|
info: '',
|
||||||
|
meta: null,
|
||||||
|
block: true,
|
||||||
|
hidden: false
|
||||||
|
};
|
||||||
|
tokens.splice(i + 2, 0, closeToken);
|
||||||
|
|
||||||
|
// Remove the original HTML token
|
||||||
|
tokens.splice(i, 1);
|
||||||
|
i--; // Adjust index since we removed a token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Try to fix Mermaid Diagram error: Maximum call stack size exceeded.
|
// Try to fix Mermaid Diagram error: Maximum call stack size exceeded.
|
||||||
// Added bigger text size for Diagram.
|
// Added bigger text size for Diagram.
|
||||||
// https://github.com/wekan/wekan/issues/4251
|
// https://github.com/wekan/wekan/issues/4251
|
||||||
|
|
@ -69,12 +238,12 @@ if (Package.ui) {
|
||||||
// Prevent hiding info: https://wekan.github.io/hall-of-fame/invisiblebleed/
|
// Prevent hiding info: https://wekan.github.io/hall-of-fame/invisiblebleed/
|
||||||
// If markdown link does not have description, do not render markdown, instead show all of markdown source code using preformatted text.
|
// If markdown link does not have description, do not render markdown, instead show all of markdown source code using preformatted text.
|
||||||
// Also show html comments.
|
// Also show html comments.
|
||||||
return HTML.Raw('<pre style="background-color: red;" title="Warning! Hidden markdown link description!" aria-label="Warning! Hidden markdown link description!">' + DOMPurify.sanitize(text.replace('<!--', '<!--').replace('-->', '-->')) + '</pre>');
|
return HTML.Raw('<pre style="background-color: red;" title="Warning! Hidden markdown link description!" aria-label="Warning! Hidden markdown link description!">' + DOMPurify.sanitize(text.replace('<!--', '<!--').replace('-->', '-->'), getSecureDOMPurifyConfig()) + '</pre>');
|
||||||
} else {
|
} else {
|
||||||
// Prevent hiding info: https://wekan.github.io/hall-of-fame/invisiblebleed/
|
// Prevent hiding info: https://wekan.github.io/hall-of-fame/invisiblebleed/
|
||||||
// If text does not have hidden markdown link, render all markdown.
|
// If text does not have hidden markdown link, render all markdown.
|
||||||
// Also show html comments.
|
// Also show html comments.
|
||||||
return HTML.Raw(DOMPurify.sanitize(Markdown.render(text).replace('<!--', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!"><!--</font>').replace('-->', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!">--></font>'), {ALLOW_UNKNOWN_PROTOCOLS: true}));
|
return HTML.Raw(DOMPurify.sanitize(Markdown.render(text).replace('<!--', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!"><!--</font>').replace('-->', '<font color="red" title="Warning! Hidden HTML comment!" aria-label="Warning! Hidden HTML comment!">--></font>'), getSecureDOMPurifyConfig()));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue