diff --git a/src/components/ha-markdown-element.ts b/src/components/ha-markdown-element.ts index 1c642df869..cbc6b81755 100644 --- a/src/components/ha-markdown-element.ts +++ b/src/components/ha-markdown-element.ts @@ -26,6 +26,9 @@ class HaMarkdownElement extends ReactiveElement { @property({ attribute: "allow-svg", type: Boolean }) public allowSvg = false; + @property({ attribute: "allow-data-url", type: Boolean }) + public allowDataUrl = false; + @property({ type: Boolean }) public breaks = false; @property({ type: Boolean, attribute: "lazy-images" }) public lazyImages = @@ -66,6 +69,7 @@ class HaMarkdownElement extends ReactiveElement { return hash({ content: this.content, allowSvg: this.allowSvg, + allowDataUrl: this.allowDataUrl, breaks: this.breaks, }); } @@ -79,6 +83,7 @@ class HaMarkdownElement extends ReactiveElement { }, { allowSvg: this.allowSvg, + allowDataUrl: this.allowDataUrl, } ); diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts index 5bc9c26b74..51d4fdd49e 100644 --- a/src/components/ha-markdown.ts +++ b/src/components/ha-markdown.ts @@ -8,6 +8,9 @@ export class HaMarkdown extends LitElement { @property({ attribute: "allow-svg", type: Boolean }) public allowSvg = false; + @property({ attribute: "allow-data-url", type: Boolean }) + public allowDataUrl = false; + @property({ type: Boolean }) public breaks = false; @property({ type: Boolean, attribute: "lazy-images" }) public lazyImages = @@ -23,6 +26,7 @@ export class HaMarkdown extends LitElement { return html` + ` : ""; }, diff --git a/src/resources/markdown-worker.ts b/src/resources/markdown-worker.ts index 1a8d3e3dc1..ae8c5a3a68 100644 --- a/src/resources/markdown-worker.ts +++ b/src/resources/markdown-worker.ts @@ -7,34 +7,13 @@ import { filterXSS, getDefaultWhiteList } from "xss"; let whiteListNormal: IWhiteList | undefined; let whiteListSvg: IWhiteList | undefined; -// Override the default `onTagAttr` behavior to only render -// our markdown checkboxes. -// Returning undefined causes the default measure to be taken -// in the xss library. -const onTagAttr = ( - tag: string, - name: string, - value: string -): string | undefined => { - if (tag === "input") { - if ( - (name === "type" && value === "checkbox") || - name === "checked" || - name === "disabled" - ) { - return undefined; - } - return ""; - } - return undefined; -}; - const renderMarkdown = async ( content: string, markedOptions: MarkedOptions, hassOptions: { // Do not allow SVG on untrusted content, it allows XSS. allowSvg?: boolean; + allowDataUrl?: boolean; } = {} ): Promise => { if (!whiteListNormal) { @@ -70,10 +49,41 @@ const renderMarkdown = async ( } else { whiteList = whiteListNormal; } + if (hassOptions.allowDataUrl && whiteList.a) { + whiteList.a.push("download"); + } return filterXSS(await marked(content, markedOptions), { whiteList, - onTagAttr, + onTagAttr: ( + tag: string, + name: string, + value: string + ): string | undefined => { + // Override the default `onTagAttr` behavior to only render + // our markdown checkboxes. + // Returning undefined causes the default measure to be taken + // in the xss library. + if (tag === "input") { + if ( + (name === "type" && value === "checkbox") || + name === "checked" || + name === "disabled" + ) { + return undefined; + } + return ""; + } + if ( + hassOptions.allowDataUrl && + tag === "a" && + name === "href" && + value.startsWith("data:") + ) { + return `href="${value}"`; + } + return undefined; + }, }); };