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