You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-11-29 22:48:10 +02:00
All: Security: Fixed potential Arbitrary File Read via XSS
This commit is contained in:
@@ -1,33 +1,50 @@
|
||||
const htmlUtils = require('./htmlUtils');
|
||||
const utils = require('./utils');
|
||||
const noteStyle = require('./noteStyle');
|
||||
const memoryCache = require('memory-cache');
|
||||
const md5 = require('md5');
|
||||
|
||||
class HtmlToHtml {
|
||||
constructor(options) {
|
||||
if (!options) options = {};
|
||||
this.resourceBaseUrl_ = 'resourceBaseUrl' in options ? options.resourceBaseUrl : null;
|
||||
this.ResourceModel_ = options.ResourceModel;
|
||||
this.cache_ = new memoryCache.Cache();
|
||||
}
|
||||
|
||||
render(markup, theme, options) {
|
||||
const html = htmlUtils.processImageTags(markup, data => {
|
||||
if (!data.src) return null;
|
||||
async render(markup, theme, options) {
|
||||
const cacheKey = md5(escape(markup));
|
||||
let html = this.cache_.get(cacheKey);
|
||||
|
||||
const r = utils.imageReplacement(this.ResourceModel_, data.src, options.resources, this.resourceBaseUrl_);
|
||||
if (!r) return null;
|
||||
if (!html) {
|
||||
html = htmlUtils.sanitizeHtml(markup);
|
||||
|
||||
if (typeof r === 'string') {
|
||||
return {
|
||||
type: 'replaceElement',
|
||||
html: r,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'setAttributes',
|
||||
attrs: r,
|
||||
};
|
||||
}
|
||||
});
|
||||
html = htmlUtils.processImageTags(html, data => {
|
||||
if (!data.src) return null;
|
||||
|
||||
const r = utils.imageReplacement(this.ResourceModel_, data.src, options.resources, this.resourceBaseUrl_);
|
||||
if (!r) return null;
|
||||
|
||||
if (typeof r === 'string') {
|
||||
return {
|
||||
type: 'replaceElement',
|
||||
html: r,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
type: 'setAttributes',
|
||||
attrs: r,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (options.bodyOnly) return {
|
||||
html: html,
|
||||
pluginAssets: [],
|
||||
};
|
||||
|
||||
this.cache_.put(cacheKey, html, 1000 * 60 * 10);
|
||||
|
||||
const cssStrings = noteStyle(theme, options);
|
||||
const styleHtml = `<style>${cssStrings.join('\n')}</style>`;
|
||||
|
||||
@@ -33,7 +33,7 @@ class MarkupToHtml {
|
||||
return '';
|
||||
}
|
||||
|
||||
render(markupLanguage, markup, theme, options) {
|
||||
async render(markupLanguage, markup, theme, options) {
|
||||
return this.renderer(markupLanguage).render(markup, theme, options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ const MarkdownIt = require('markdown-it');
|
||||
const md5 = require('md5');
|
||||
const noteStyle = require('./noteStyle');
|
||||
const { fileExtension } = require('./pathUtils');
|
||||
const memoryCache = require('memory-cache');
|
||||
const rules = {
|
||||
image: require('./MdToHtml/rules/image'),
|
||||
checkbox: require('./MdToHtml/rules/checkbox'),
|
||||
@@ -12,6 +13,7 @@ const rules = {
|
||||
code_inline: require('./MdToHtml/rules/code_inline'),
|
||||
fountain: require('./MdToHtml/rules/fountain'),
|
||||
mermaid: require('./MdToHtml/rules/mermaid').default,
|
||||
sanitize_html: require('./MdToHtml/rules/sanitize_html').default,
|
||||
};
|
||||
const setupLinkify = require('./MdToHtml/setupLinkify');
|
||||
const hljs = require('highlight.js');
|
||||
@@ -50,6 +52,7 @@ class MdToHtml {
|
||||
this.cachedHighlightedCode_ = {};
|
||||
this.ResourceModel_ = options.ResourceModel;
|
||||
this.pluginOptions_ = options.pluginOptions ? options.pluginOptions : {};
|
||||
this.contextCache_ = new memoryCache.Cache();
|
||||
}
|
||||
|
||||
pluginOptions(name) {
|
||||
@@ -106,6 +109,7 @@ class MdToHtml {
|
||||
|
||||
async render(body, style = null, options = null) {
|
||||
if (!options) options = {};
|
||||
if (!('bodyOnly' in options)) options.bodyOnly = false;
|
||||
if (!options.postMessageSyntax) options.postMessageSyntax = 'postMessage';
|
||||
if (!options.paddingBottom) options.paddingBottom = '0';
|
||||
if (!options.highlightedKeywords) options.highlightedKeywords = [];
|
||||
@@ -129,6 +133,7 @@ class MdToHtml {
|
||||
const context = {
|
||||
css: {},
|
||||
pluginAssets: {},
|
||||
cache: this.contextCache_,
|
||||
};
|
||||
|
||||
const ruleOptions = Object.assign({}, options, {
|
||||
@@ -203,6 +208,7 @@ class MdToHtml {
|
||||
if (this.pluginEnabled('katex')) markdownIt.use(rules.katex(context, ruleOptions));
|
||||
if (this.pluginEnabled('fountain')) markdownIt.use(rules.fountain(context, ruleOptions));
|
||||
if (this.pluginEnabled('mermaid')) markdownIt.use(rules.mermaid(context, ruleOptions));
|
||||
markdownIt.use(rules.sanitize_html(context, ruleOptions));
|
||||
markdownIt.use(rules.highlight_keywords(context, ruleOptions));
|
||||
markdownIt.use(rules.code_inline(context, ruleOptions));
|
||||
markdownIt.use(markdownItAnchor, { slugify: uslugify });
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
const md5 = require('md5');
|
||||
const htmlUtils = require('../../htmlUtils');
|
||||
|
||||
// @ts-ignore: Keep the function signature as-is despite unusued arguments
|
||||
function installRule(markdownIt:any, mdOptions:any, ruleOptions:any, context:any) {
|
||||
markdownIt.core.ruler.push('sanitize_html', (state:any) => {
|
||||
const tokens = state.tokens;
|
||||
|
||||
const walkHtmlTokens = (tokens:any[]) => {
|
||||
if (!tokens || !tokens.length) return;
|
||||
|
||||
for (const token of tokens) {
|
||||
if (!['html_block', 'html_inline'].includes(token.type)) {
|
||||
walkHtmlTokens(token.children);
|
||||
continue;
|
||||
}
|
||||
|
||||
const cacheKey = md5(escape(token.content));
|
||||
let sanitizedContent = context.cache.get(cacheKey);
|
||||
|
||||
if (!sanitizedContent) {
|
||||
sanitizedContent = htmlUtils.sanitizeHtml(token.content);
|
||||
}
|
||||
|
||||
token.content = sanitizedContent;
|
||||
|
||||
context.cache.put(cacheKey, sanitizedContent, 1000 * 60 * 60);
|
||||
walkHtmlTokens(token.children);
|
||||
}
|
||||
};
|
||||
|
||||
walkHtmlTokens(tokens);
|
||||
});
|
||||
}
|
||||
|
||||
export default function(context:any, ruleOptions:any) {
|
||||
return function(md:any, mdOptions:any) {
|
||||
installRule(md, mdOptions, ruleOptions, context);
|
||||
};
|
||||
}
|
||||
@@ -2,8 +2,11 @@ const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = new Entities().encode;
|
||||
|
||||
// [\s\S] instead of . for multiline matching
|
||||
const NodeHtmlParser = require('node-html-parser');
|
||||
|
||||
// https://stackoverflow.com/a/16119722/561309
|
||||
const imageRegex = /<img([\s\S]*?)src=["']([\s\S]*?)["']([\s\S]*?)>/gi;
|
||||
const JS_EVENT_NAMES = ['onabort', 'onafterprint', 'onbeforeprint', 'onbeforeunload', 'onblur', 'oncanplay', 'oncanplaythrough', 'onchange', 'onclick', 'oncontextmenu', 'oncopy', 'oncuechange', 'oncut', 'ondblclick', 'ondrag', 'ondragend', 'ondragenter', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onended', 'onerror', 'onfocus', 'onhashchange', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadstart', 'onmessage', 'onmousedown', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onoffline', 'ononline', 'onpagehide', 'onpageshow', 'onpaste', 'onpause', 'onplay', 'onplaying', 'onpopstate', 'onprogress', 'onratechange', 'onreset', 'onresize', 'onscroll', 'onsearch', 'onseeked', 'onseeking', 'onselect', 'onstalled', 'onstorage', 'onsubmit', 'onsuspend', 'ontimeupdate', 'ontoggle', 'onunload', 'onvolumechange', 'onwaiting', 'onwheel'];
|
||||
|
||||
class HtmlUtils {
|
||||
|
||||
@@ -43,6 +46,34 @@ class HtmlUtils {
|
||||
});
|
||||
}
|
||||
|
||||
sanitizeHtml(html) {
|
||||
const walkHtmlNodes = (nodes) => {
|
||||
if (!nodes || !nodes.length) return;
|
||||
|
||||
for (const node of nodes) {
|
||||
for (const attr in node.attributes) {
|
||||
if (!node.attributes.hasOwnProperty(attr)) continue;
|
||||
if (JS_EVENT_NAMES.includes(attr)) node.setAttribute(attr, '');
|
||||
}
|
||||
walkHtmlNodes(node.childNodes);
|
||||
}
|
||||
};
|
||||
|
||||
// Need to wrap in div, otherwise elements at the root will be skipped
|
||||
// The DIV tags are removed below
|
||||
const dom = NodeHtmlParser.parse(`<div>${html}</div>`, {
|
||||
script: false,
|
||||
style: true,
|
||||
pre: true,
|
||||
comment: false,
|
||||
});
|
||||
|
||||
walkHtmlNodes([dom]);
|
||||
const output = dom.toString();
|
||||
return output.substr(5, output.length - 11);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
const htmlUtils = new HtmlUtils();
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
"markdown-it-toc-done-right": "^4.1.0",
|
||||
"md5": "^2.2.1",
|
||||
"mermaid": "^8.4.6",
|
||||
"memory-cache": "^0.2.0",
|
||||
"node-html-parser": "^1.2.4",
|
||||
"uslug": "^1.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user