diff --git a/src/parser/config/config.ts b/src/parser/config/config.ts
index fde821a0ca..ceaa7b4377 100644
--- a/src/parser/config/config.ts
+++ b/src/parser/config/config.ts
@@ -32,11 +32,25 @@ export interface HTMLParserOptions {
*/
allowUnsafeAttr?: boolean;
+ /**
+ * Allow unsafe HTML attribute values (eg. `src="javascript:..."`).
+ * @default false
+ */
+ allowUnsafeAttrValue?: boolean;
+
/**
* When false, removes empty text nodes when parsed, unless they contain a space.
* @default false
*/
keepEmptyTextNodes?: boolean;
+
+ /**
+ * Custom transformer to run before passing the input HTML to the parser.
+ * A common use case might be to sanitize the input string.
+ * @example
+ * preParser: htmlString => DOMPurify.sanitize(htmlString)
+ */
+ preParser?: (input: string, opts: { editor: Editor }) => string;
}
export interface ParserConfig {
@@ -84,6 +98,7 @@ const config: ParserConfig = {
htmlType: 'text/html',
allowScripts: false,
allowUnsafeAttr: false,
+ allowUnsafeAttrValue: false,
keepEmptyTextNodes: false,
},
};
diff --git a/src/parser/model/ParserHtml.ts b/src/parser/model/ParserHtml.ts
index 42482c27ad..bacb66e6c7 100644
--- a/src/parser/model/ParserHtml.ts
+++ b/src/parser/model/ParserHtml.ts
@@ -310,7 +310,9 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
htmlType: config.optionsHtml?.htmlType || config.htmlType,
...opts,
};
- const el = isFunction(cf.parserHtml) ? cf.parserHtml(str, options) : BrowserParserHtml(str, options);
+ const { preParser } = options;
+ const input = isFunction(preParser) ? preParser(str, { editor: em?.getEditor()! }) : str;
+ const el = isFunction(cf.parserHtml) ? cf.parserHtml(input, options) : BrowserParserHtml(input, options);
const scripts = el.querySelectorAll('script');
let i = scripts.length;
@@ -323,8 +325,8 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
}
// Remove unsafe attributes
- if (!options.allowUnsafeAttr) {
- this.__clearUnsafeAttr(el);
+ if (!options.allowUnsafeAttr || !options.allowUnsafeAttrValue) {
+ this.__sanitizeNode(el, options);
}
// Detach style tags and parse them
@@ -341,26 +343,28 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
if (styleStr) res.css = parserCss.parse(styleStr);
}
- em && em.trigger(`${event}:root`, { input: str, root: el });
+ em?.trigger(`${event}:root`, { input, root: el });
const result = this.parseNode(el, cf);
// I have to keep it otherwise it breaks the DomComponents.addComponent (returns always array)
const resHtml = result.length === 1 && !cf.returnArray ? result[0] : result;
res.html = resHtml;
- em && em.trigger(event, { input: str, output: res });
+ em?.trigger(event, { input, output: res });
return res;
},
- __clearUnsafeAttr(node: HTMLElement) {
+ __sanitizeNode(node: HTMLElement, opts: HTMLParserOptions) {
const attrs = node.attributes || [];
const nodes = node.childNodes || [];
const toRemove: string[] = [];
each(attrs, attr => {
const name = attr.nodeName || '';
- name.indexOf('on') === 0 && toRemove.push(name);
+ const value = attr.nodeValue || '';
+ !opts.allowUnsafeAttr && name.startsWith('on') && toRemove.push(name);
+ !opts.allowUnsafeAttrValue && value.startsWith('javascript:') && toRemove.push(name);
});
toRemove.map(name => node.removeAttribute(name));
- each(nodes, node => this.__clearUnsafeAttr(node as HTMLElement));
+ each(nodes, node => this.__sanitizeNode(node as HTMLElement, opts));
},
};
};
diff --git a/test/specs/parser/model/ParserHtml.ts b/test/specs/parser/model/ParserHtml.ts
index 3a8fe7d331..393ddc0384 100644
--- a/test/specs/parser/model/ParserHtml.ts
+++ b/test/specs/parser/model/ParserHtml.ts
@@ -600,4 +600,59 @@ describe('ParserHtml', () => {
];
expect(obj.parse(str).html).toEqual(result);
});
+
+ describe('Options', () => {
+ test('Remove unsafe attributes', () => {
+ const str = '';
+ const result = {
+ type: 'image',
+ tagName: 'img',
+ classes: ['test'],
+ attributes: {
+ src: 'path/img',
+ 'data-test': '1',
+ },
+ };
+ expect(obj.parse(str).html).toEqual([result]);
+ expect(obj.parse(str, null, { allowUnsafeAttr: true }).html).toEqual([
+ {
+ ...result,
+ attributes: {
+ ...result.attributes,
+ onload: 'unsafe',
+ },
+ },
+ ]);
+ });
+
+ test('Remove unsafe attribute values', () => {
+ const str = '';
+ const result = {
+ type: 'iframe',
+ tagName: 'iframe',
+ };
+ expect(obj.parse(str).html).toEqual([result]);
+ expect(obj.parse(str, null, { allowUnsafeAttrValue: true }).html).toEqual([
+ {
+ ...result,
+ attributes: {
+ src: 'javascript:alert(1)',
+ },
+ },
+ ]);
+ });
+
+ test('Custom preParser option', () => {
+ const str = '';
+ const result = {
+ type: 'iframe',
+ tagName: 'iframe',
+ attributes: {
+ src: 'test:alert(1)',
+ },
+ };
+ const preParser = (str: string) => str.replace('javascript:', 'test:');
+ expect(obj.parse(str, null, { preParser }).html).toEqual([result]);
+ });
+ });
});