1
0
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:
Laurent Cozic
2020-02-13 23:59:23 +00:00
parent 06d807d9ff
commit 3db47b575b
24 changed files with 434 additions and 95 deletions

View File

@@ -539,27 +539,6 @@ class BaseApplication {
return `${os.homedir()}/.config/${Setting.value('appName')}`;
}
async testing() {
const markdownUtils = require('lib/markdownUtils');
const ClipperServer = require('lib/ClipperServer');
const server = new ClipperServer();
const HtmlToMd = require('lib/HtmlToMd');
const service = new HtmlToMd();
const html = await shim.fsDriver().readFile('/mnt/d/test.html');
let markdown = service.parse(html, { baseUrl: 'https://duckduckgo.com/' });
console.info(markdown);
console.info('--------------------------------------------------');
const imageUrls = markdownUtils.extractImageUrls(markdown);
let result = await server.downloadImages_(imageUrls);
result = await server.createResourcesFromPaths_(result);
console.info(result);
markdown = server.replaceImageUrlsByResources_(markdown, result);
console.info('--------------------------------------------------');
console.info(markdown);
console.info('--------------------------------------------------');
}
async start(argv) {
let startFlags = await this.handleStartFlags_(argv);

View File

@@ -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>`;

View File

@@ -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);
}
}

View File

@@ -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 });

View File

@@ -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);
};
}

View File

@@ -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();

View File

@@ -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"
}
}

View File

@@ -214,7 +214,7 @@ function shimInit() {
if (shim.isElectron()) {
const nativeImage = require('electron').nativeImage;
let image = nativeImage.createFromDataURL(imageDataUrl);
if (image.isEmpty()) throw new Error('Could not convert data URL to image'); // Would throw for example if the image format is no supported (eg. image/gif)
if (image.isEmpty()) throw new Error('Could not convert data URL to image - perhaps the format is not supported (eg. image/gif)'); // Would throw for example if the image format is no supported (eg. image/gif)
if (options.cropRect) {
// Crop rectangle values need to be rounded or the crop() call will fail
const c = options.cropRect;

View File

@@ -5689,6 +5689,11 @@
"mimic-fn": "^1.0.0"
}
},
"memory-cache": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz",
"integrity": "sha1-eJCwHVLADI68nVM+H46xfjA0hxo="
},
"merge-stream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz",
@@ -6380,6 +6385,21 @@
"is-stream": "^1.0.1"
}
},
"node-html-parser": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.2.4.tgz",
"integrity": "sha512-qHwPdGyGr9pOZBoSgUOuNPG20QYZVN00lFcxKQgjPUODSxVH7obQeLVVawa3B4cfSNtLIeczSzoy/xYA8XG5WQ==",
"requires": {
"he": "1.1.1"
},
"dependencies": {
"he": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
"integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0="
}
}
},
"node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",

View File

@@ -72,6 +72,8 @@
"react-native-version-info": "^0.5.1",
"react-native-webview": "^5.12.0",
"react-redux": "5.0.7",
"memory-cache": "^0.2.0",
"node-html-parser": "^1.2.4",
"redux": "4.0.0",
"reselect": "^4.0.0",
"rn-fetch-blob": "^0.12.0",