CVE-2020-9038 (JavaScript, published 2020-02-17T16:15Z)
Description: value: Joplin through 1.0.184 allows Arbitrary File Read via XSS.
Severity: LOW (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/laurent22/joplin
Files changed:
.eslintignore
.gitignore
CliClient/package-lock.json
CliClient/package.json
CliClient/tests/HtmlToHtml.js
CliClient/tests/MdToHtml.js
CliClient/tests/html_to_html/sanitize.dest.html
CliClient/tests/html_to_html/sanitize.src.html
CliClient/tests/md_to_html/sanitize.html
CliClient/tests/md_to_html/sanitize.md
Clipper/joplin-webclipper/content_scripts/index.js
ElectronClient/app/package-lock.json
ElectronClient/app/package.json
ElectronClient/build.sh
ReactNativeClient/lib/BaseApplication.js
ReactNativeClient/lib/joplin-renderer/HtmlToHtml.js
ReactNativeClient/lib/joplin-renderer/MarkupToHtml.js
ReactNativeClient/lib/joplin-renderer/MdToHtml.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.ts
ReactNativeClient/lib/joplin-renderer/htmlUtils.js
ReactNativeClient/lib/joplin-renderer/package.json
ReactNativeClient/lib/shim-init-node.js
ReactNativeClient/package-lock.json
ReactNativeClient/package.json
Commit message: All: Security: Fixed potential Arbitrary File Read via XSS


Vulnerable code:
    "yargs-parser": "^7.0.0"
		if (['http', 'https', 'file'].indexOf(protocol) >= 0) return url;
      "version": "0.2.1",
      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.1.tgz",
      "integrity": "sha512-sK3ujri04WyjwQXVoK4PU3y8ula1stq10GJZpqHIUgoGZdsGzAGu65BnU3d08aTVSvO7mGPZUc0wTEDL+qGE0Q==",
        "domelementtype": {
          "version": "2.0.1",
          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
          "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
        },
      "version": "1.3.1",
      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
      "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
      "version": "2.4.2",
      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
      "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
        "domelementtype": "1"
      "version": "1.7.0",
      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
      "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
        "dom-serializer": "0",
        "domelementtype": "1"
      "version": "3.10.1",
      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
      "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
      "requires": {
        "domelementtype": "^1.3.1",
        "domhandler": "^2.3.0",
        "domutils": "^1.5.1",
        "entities": "^1.1.1",
        "inherits": "^2.0.1",
        "readable-stream": "^3.1.1"
        "readable-stream": {
          "version": "3.4.0",
          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz",
          "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==",
          "requires": {
            "inherits": "^2.0.3",
            "string_decoder": "^1.1.1",
            "util-deprecate": "^1.0.1"
          }
        },
        "safe-buffer": {
          "version": "5.2.0",
          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
          "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
        },
        "string_decoder": {
          "version": "1.3.0",
          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
          "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
          "requires": {
            "safe-buffer": "~5.2.0"
          }
cd "$ROOT_DIR/.."
npm run tsc
	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('--------------------------------------------------');
	}

	render(markup, theme, options) {
		const html = htmlUtils.processImageTags(markup, 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,
				};
			}
		});
	render(markupLanguage, markup, theme, options) {
			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)

Fixed code:
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
ReactNativeClient/lib/joplin-renderer/MdToHtml/rules/sanitize_html.js
    "memory-cache": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz",
      "integrity": "sha1-eJCwHVLADI68nVM+H46xfjA0hxo="
    },
    "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==",
      "dev": true,
      "requires": {
        "he": "1.1.1"
      }
    },
    "memory-cache": "^0.2.0",
    "yargs-parser": "^7.0.0",
    "node-html-parser": "^1.2.4"
/* eslint-disable no-unused-vars */

require('app-module-path').addPath(__dirname);

const os = require('os');
const { time } = require('lib/time-utils.js');
const { filename } = require('lib/path-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim');
const HtmlToHtml = require('lib/joplin-renderer/HtmlToHtml');
const { enexXmlToMd } = require('lib/import-enex-md-gen.js');

jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit

process.on('unhandledRejection', (reason, p) => {
	console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});

describe('HtmlToHtml', function() {

	beforeEach(async (done) => {
		await setupDatabaseAndSynchronizer(1);
		await switchClient(1);
		done();
	});

	it('should convert from Html to Html', asyncTest(async () => {
		const basePath = `${__dirname}/html_to_html`;
		const files = await shim.fsDriver().readDirStats(basePath);
		const htmlToHtml = new HtmlToHtml();

		for (let i = 0; i < files.length; i++) {
			const htmlSourceFilename = files[i].path;
			if (htmlSourceFilename.indexOf('.src.html') < 0) continue;

			const htmlSourceFilePath = `${basePath}/${htmlSourceFilename}`;
			const htmlDestPath = `${basePath}/${filename(filename(htmlSourceFilePath))}.dest.html`;

			// if (htmlSourceFilename !== 'table_with_header.html') continue;

			const htmlToHtmlOptions = {
				bodyOnly: true,
			};

			const sourceHtml = await shim.fsDriver().readFile(htmlSourceFilePath);
			let expectedHtml = await shim.fsDriver().readFile(htmlDestPath);

			const result = await htmlToHtml.render(sourceHtml, null, htmlToHtmlOptions);
			let actualHtml = result.html;

			if (os.EOL === '\r\n') {
				expectedHtml = expectedHtml.replace(/\r\n/g, '\n');
				actualHtml = actualHtml.replace(/\r\n/g, '\n');
			}

			if (actualHtml !== expectedHtml) {
				console.info('');
				console.info(`Error converting file: ${htmlSourceFilename}`);
				console.info('--------------------------------- Got:');
				console.info(actualHtml);
				console.info('--------------------------------- Raw:');
				console.info(actualHtml.split('\n'));
				console.info('--------------------------------- Expected:');
				console.info(expectedHtml.split('\n'));
				console.info('--------------------------------------------');
				console.info('');

				expect(false).toBe(true);
				// return;
			} else {
				expect(true).toBe(true);
			}
		}
	}));

});
/* eslint-disable no-unused-vars */

require('app-module-path').addPath(__dirname);

const os = require('os');
const { time } = require('lib/time-utils.js');
const { filename } = require('lib/path-utils.js');
const { asyncTest, fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synchronizer, fileApi, sleep, clearDatabase, switchClient, syncTargetId, objectsEqual, checkThrowAsync } = require('test-utils.js');
const Folder = require('lib/models/Folder.js');
const Note = require('lib/models/Note.js');
const BaseModel = require('lib/BaseModel.js');
const { shim } = require('lib/shim');
const MdToHtml = require('lib/joplin-renderer/MdToHtml');
const { enexXmlToMd } = require('lib/import-enex-md-gen.js');

jasmine.DEFAULT_TIMEOUT_INTERVAL = 60 * 60 * 1000; // Can run for a while since everything is in the same test unit

process.on('unhandledRejection', (reason, p) => {
	console.log('Unhandled Rejection at: Promise', p, 'reason:', reason);
});

describe('MdToHtml', function() {

	beforeEach(async (done) => {
		await setupDatabaseAndSynchronizer(1);
		await switchClient(1);
		done();
	});

	it('should convert from Markdown to Html', asyncTest(async () => {
		const basePath = `${__dirname}/md_to_html`;
		const files = await shim.fsDriver().readDirStats(basePath);
		const mdToHtml = new MdToHtml();

		for (let i = 0; i < files.length; i++) {
			const mdFilename = files[i].path;
			if (mdFilename.indexOf('.md') < 0) continue;

			const mdFilePath = `${basePath}/${mdFilename}`;
			const htmlPath = `${basePath}/${filename(mdFilePath)}.html`;

			// if (mdFilename !== 'table_with_header.html') continue;

			const mdToHtmlOptions = {
				bodyOnly: true,
			};

			const markdown = await shim.fsDriver().readFile(mdFilePath);
			let expectedHtml = await shim.fsDriver().readFile(htmlPath);

			const result = await mdToHtml.render(markdown, null, mdToHtmlOptions);
			let actualHtml = result.html;

			if (os.EOL === '\r\n') {
				expectedHtml = expectedHtml.replace(/\r\n/g, '\n');
				actualHtml = actualHtml.replace(/\r\n/g, '\n');
			}

			if (actualHtml !== expectedHtml) {
				console.info('');
				console.info(`Error converting file: ${mdFilename}`);
				console.info('--------------------------------- Got:');
				console.info(actualHtml);
				console.info('--------------------------------- Raw:');
				console.info(actualHtml.split('\n'));
				console.info('--------------------------------- Expected:');
				console.info(expectedHtml.split('\n'));
				console.info('--------------------------------------------');
				console.info('');

				expect(false).toBe(true);
				// return;
			} else {
				expect(true).toBe(true);
			}
		}
	}));

});
<img src onerror="" />
<img src onerror="" />
<img src="" onerror="alert('ohno')"/>
<img src=""
   onerror="alert('ohno')"/>
<img src onerror="" />
<img src onerror="" />
<img src="" onerror="alert('ohno')"/>
<img src=""
   onerror="alert('ohno')"/>
		if (['http', 'https', 'file', 'data'].indexOf(protocol) >= 0) return url;
        "domelementtype": {
          "version": "1.3.1",
          "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
          "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w=="
        },
        "domhandler": {
          "version": "2.4.2",
          "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
          "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
          "requires": {
            "domelementtype": "1"
          }
        },
        "domutils": {
          "version": "1.7.0",
          "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
          "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
          "requires": {
            "dom-serializer": "0",
            "domelementtype": "1"
          }
        },
        "htmlparser2": {
          "version": "3.10.1",
          "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
          "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
          "requires": {
            "domelementtype": "^1.3.1",
            "domhandler": "^2.3.0",
            "domutils": "^1.5.1",
            "entities": "^1.1.1",
            "inherits": "^2.0.1",
            "readable-stream": "^3.1.1"
          }
        },
        },
        "readable-stream": {
          "version": "3.6.0",
          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
          "requires": {
            "inherits": "^2.0.3",
            "string_decoder": "^1.1.1",
            "util-deprecate": "^1.0.1"
          }
        },
        "safe-buffer": {
          "version": "5.2.0",
          "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz",
          "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg=="
        },
        "string_decoder": {
          "version": "1.3.0",
          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
          "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
          "requires": {
            "safe-buffer": "~5.2.0"
          }
      "version": "0.2.2",
      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
      "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
      "version": "2.0.1",
      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz",
      "integrity": "sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ=="
      "version": "3.0.0",
      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.0.0.tgz",
      "integrity": "sha512-eKLdI5v9m67kbXQbJSNn1zjh0SDzvzWVWtX+qEI3eMjZw8daH9k8rlj1FZY9memPwjiskQFbe7vHVVJIAqoEhw==",
        "domelementtype": "^2.0.1"
      "version": "2.0.0",
      "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.0.0.tgz",
      "integrity": "sha512-n5SelJ1axbO636c2yUtOGia/IcJtVtlhQbFiVDBZHKV5ReJO1ViX7sFEemtuyoAnBxk5meNSYgA8V4s0271efg==",
        "dom-serializer": "^0.2.1",
        "domelementtype": "^2.0.1",
        "domhandler": "^3.0.0"
      "version": "4.0.0",
      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.0.0.tgz",
      "integrity": "sha512-cChwXn5Vam57fyXajDtPXL1wTYc8JtLbr2TN76FYu05itVVVealxLowe2B3IEznJG4p9HAYn/0tJaRlGuEglFQ==",
      "requires": {
        "domelementtype": "^2.0.1",
        "domhandler": "^3.0.0",
        "domutils": "^2.0.0",
        "entities": "^2.0.0"
        "entities": {
          "version": "2.0.0",
          "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.0.tgz",
          "integrity": "sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw=="
    "memory-cache": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz",
      "integrity": "sha1-eJCwHVLADI68nVM+H46xfjA0hxo="
    },
    "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="
        }
      }
    },
    "memory-cache": "^0.2.0",
    "node-html-parser": "^1.2.4",
# cd "$ROOT_DIR/.."
# npm run tsc
const memoryCache = require('memory-cache');
const md5 = require('md5');
		this.cache_ = new memoryCache.Cache();
	async render(markup, theme, options) {
		const cacheKey = md5(escape(markup));
		let html = this.cache_.get(cacheKey);

		if (!html) {
			html = htmlUtils.sanitizeHtml(markup);

			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);
	async render(markupLanguage, markup, theme, options) {
const memoryCache = require('memory-cache');
	sanitize_html: require('./MdToHtml/rules/sanitize_html').default,
		this.contextCache_ = new memoryCache.Cache();
		if (!('bodyOnly' in options)) options.bodyOnly = false;
			cache: this.contextCache_,
		markdownIt.use(rules.sanitize_html(context, ruleOptions));
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);
	};
}
const NodeHtmlParser = require('node-html-parser');

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'];
	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);
	}


    "memory-cache": "^0.2.0",
    "node-html-parser": "^1.2.4",
			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)
    "memory-cache": {
      "version": "0.2.0",
      "resolved": "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz",
      "integrity": "sha1-eJCwHVLADI68nVM+H46xfjA0hxo="
    },
    "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="
        }
      }
    },
    "memory-cache": "^0.2.0",
    "node-html-parser": "^1.2.4",