From 119f756c0e032683647bbf197bc93052e8e41ea2 Mon Sep 17 00:00:00 2001 From: Coleman Rollins Date: Tue, 21 Dec 2021 10:32:48 -0600 Subject: [PATCH] [cr] whitelist flexbox styles in text panel editor (#43222) * [cr] whitelist flexbox styles in text panel editor * [cr] separate sanitize function for text panel only * [cr] separate markdown function for text panel * [cr] common markdown options --- packages/grafana-data/src/text/index.ts | 3 +- .../grafana-data/src/text/markdown.test.ts | 10 ++++++ packages/grafana-data/src/text/markdown.ts | 32 ++++++++++++++----- packages/grafana-data/src/text/sanitize.ts | 32 +++++++++++++++++++ public/app/plugins/panel/text/TextPanel.tsx | 8 +++-- .../plugins/panel/text/TextPanelEditor.tsx | 2 +- 6 files changed, 74 insertions(+), 13 deletions(-) diff --git a/packages/grafana-data/src/text/index.ts b/packages/grafana-data/src/text/index.ts index bece68920ac..76470eaa838 100644 --- a/packages/grafana-data/src/text/index.ts +++ b/packages/grafana-data/src/text/index.ts @@ -1,11 +1,12 @@ export * from './string'; export * from './markdown'; export * from './text'; -import { escapeHtml, hasAnsiCodes, sanitize, sanitizeUrl } from './sanitize'; +import { escapeHtml, hasAnsiCodes, sanitize, sanitizeUrl, sanitizeTextPanelContent } from './sanitize'; export const textUtil = { escapeHtml, hasAnsiCodes, sanitize, + sanitizeTextPanelContent, sanitizeUrl, }; diff --git a/packages/grafana-data/src/text/markdown.test.ts b/packages/grafana-data/src/text/markdown.test.ts index ed51f2c28a2..b9e7052c3ef 100644 --- a/packages/grafana-data/src/text/markdown.test.ts +++ b/packages/grafana-data/src/text/markdown.test.ts @@ -1,4 +1,5 @@ import { renderMarkdown } from './markdown'; +import { sanitizeTextPanelContent } from './sanitize'; describe('Markdown wrapper', () => { it('should be able to handle undefined value', () => { @@ -10,4 +11,13 @@ describe('Markdown wrapper', () => { const str = renderMarkdown(''); expect(str).toBe('<script>alert()</script>'); }); + + it('should allow whitelisted styles in text panel', () => { + const html = + '
'; + const str = sanitizeTextPanelContent(html); + expect(str).toBe( + '
' + ); + }); }); diff --git a/packages/grafana-data/src/text/markdown.ts b/packages/grafana-data/src/text/markdown.ts index 9b684152a90..a87d0f0bb54 100644 --- a/packages/grafana-data/src/text/markdown.ts +++ b/packages/grafana-data/src/text/markdown.ts @@ -1,5 +1,5 @@ import { marked } from 'marked'; -import { sanitize } from './sanitize'; +import { sanitize, sanitizeTextPanelContent } from './sanitize'; let hasInitialized = false; @@ -7,15 +7,17 @@ export interface RenderMarkdownOptions { noSanitize?: boolean; } +const markdownOptions = { + pedantic: false, + gfm: true, + smartLists: true, + smartypants: false, + xhtml: false, +}; + export function renderMarkdown(str?: string, options?: RenderMarkdownOptions): string { if (!hasInitialized) { - marked.setOptions({ - pedantic: false, - gfm: true, - smartLists: true, - smartypants: false, - xhtml: false, - }); + marked.setOptions({ ...markdownOptions }); hasInitialized = true; } @@ -26,3 +28,17 @@ export function renderMarkdown(str?: string, options?: RenderMarkdownOptions): s return sanitize(html); } + +export function renderTextPanelMarkdown(str?: string, options?: RenderMarkdownOptions): string { + if (!hasInitialized) { + marked.setOptions({ ...markdownOptions }); + hasInitialized = true; + } + + const html = marked(str || ''); + if (options?.noSanitize) { + return html; + } + + return sanitizeTextPanelContent(html); +} diff --git a/packages/grafana-data/src/text/sanitize.ts b/packages/grafana-data/src/text/sanitize.ts index 2a689fbc794..b5a2393c1c4 100644 --- a/packages/grafana-data/src/text/sanitize.ts +++ b/packages/grafana-data/src/text/sanitize.ts @@ -10,6 +10,29 @@ const sanitizeXSS = new FilterXSS({ whiteList: XSSWL, }); +const sanitizeTextPanelWhitelist = new xss.FilterXSS({ + whiteList: XSSWL, + css: { + whiteList: { + ...xss.getDefaultCSSWhiteList(), + 'flex-direction': true, + 'flex-wrap': true, + 'flex-basis': true, + 'flex-grow': true, + 'flex-shrink': true, + 'flex-flow': true, + gap: true, + order: true, + 'justify-content': true, + 'justify-items': true, + 'justify-self': true, + 'align-items': true, + 'align-content': true, + 'align-self': true, + }, + }, +}); + /** * Returns string safe from XSS attacks. * @@ -26,6 +49,15 @@ export function sanitize(unsanitizedString: string): string { } } +export function sanitizeTextPanelContent(unsanitizedString: string): string { + try { + return sanitizeTextPanelWhitelist.process(unsanitizedString); + } catch (error) { + console.error('String could not be sanitized', unsanitizedString); + return 'Text string could not be sanitized'; + } +} + export function sanitizeUrl(url: string): string { return braintreeSanitizeUrl(url); } diff --git a/public/app/plugins/panel/text/TextPanel.tsx b/public/app/plugins/panel/text/TextPanel.tsx index 6828fba9a22..f0251f9d658 100644 --- a/public/app/plugins/panel/text/TextPanel.tsx +++ b/public/app/plugins/panel/text/TextPanel.tsx @@ -1,7 +1,7 @@ // Libraries import React, { PureComponent } from 'react'; import { debounce } from 'lodash'; -import { PanelProps, renderMarkdown, textUtil } from '@grafana/data'; +import { PanelProps, renderTextPanelMarkdown, textUtil } from '@grafana/data'; // Utils import config from 'app/core/config'; // Types @@ -44,7 +44,9 @@ export class TextPanel extends PureComponent { prepareMarkdown(content: string): string { // Sanitize is disabled here as we handle that after variable interpolation - return renderMarkdown(this.interpolateAndSanitizeString(content), { noSanitize: config.disableSanitizeHtml }); + return renderTextPanelMarkdown(this.interpolateAndSanitizeString(content), { + noSanitize: config.disableSanitizeHtml, + }); } interpolateAndSanitizeString(content: string): string { @@ -52,7 +54,7 @@ export class TextPanel extends PureComponent { content = replaceVariables(content, {}, 'html'); - return config.disableSanitizeHtml ? content : textUtil.sanitize(content); + return config.disableSanitizeHtml ? content : textUtil.sanitizeTextPanelContent(content); } processContent(options: PanelOptions): string { diff --git a/public/app/plugins/panel/text/TextPanelEditor.tsx b/public/app/plugins/panel/text/TextPanelEditor.tsx index 6493c4c387b..a8c1a351c2f 100644 --- a/public/app/plugins/panel/text/TextPanelEditor.tsx +++ b/public/app/plugins/panel/text/TextPanelEditor.tsx @@ -40,7 +40,7 @@ export const TextPanelEditor: FC> width={width} showMiniMap={false} showLineNumbers={false} - height="200px" + height="500px" getSuggestions={getSuggestions} /> );