[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
pull/43436/head
Coleman Rollins 3 years ago committed by GitHub
parent 6abced840d
commit 119f756c0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      packages/grafana-data/src/text/index.ts
  2. 10
      packages/grafana-data/src/text/markdown.test.ts
  3. 32
      packages/grafana-data/src/text/markdown.ts
  4. 32
      packages/grafana-data/src/text/sanitize.ts
  5. 8
      public/app/plugins/panel/text/TextPanel.tsx
  6. 2
      public/app/plugins/panel/text/TextPanelEditor.tsx

@ -1,11 +1,12 @@
export * from './string'; export * from './string';
export * from './markdown'; export * from './markdown';
export * from './text'; export * from './text';
import { escapeHtml, hasAnsiCodes, sanitize, sanitizeUrl } from './sanitize'; import { escapeHtml, hasAnsiCodes, sanitize, sanitizeUrl, sanitizeTextPanelContent } from './sanitize';
export const textUtil = { export const textUtil = {
escapeHtml, escapeHtml,
hasAnsiCodes, hasAnsiCodes,
sanitize, sanitize,
sanitizeTextPanelContent,
sanitizeUrl, sanitizeUrl,
}; };

@ -1,4 +1,5 @@
import { renderMarkdown } from './markdown'; import { renderMarkdown } from './markdown';
import { sanitizeTextPanelContent } from './sanitize';
describe('Markdown wrapper', () => { describe('Markdown wrapper', () => {
it('should be able to handle undefined value', () => { it('should be able to handle undefined value', () => {
@ -10,4 +11,13 @@ describe('Markdown wrapper', () => {
const str = renderMarkdown('<script>alert()</script>'); const str = renderMarkdown('<script>alert()</script>');
expect(str).toBe('&lt;script&gt;alert()&lt;/script&gt;'); expect(str).toBe('&lt;script&gt;alert()&lt;/script&gt;');
}); });
it('should allow whitelisted styles in text panel', () => {
const html =
'<div style="display:flex; flex-direction: column; flex-wrap: wrap; justify-content: start; gap: 2px;"><div style="flex-basis: 50%"></div></div>';
const str = sanitizeTextPanelContent(html);
expect(str).toBe(
'<div style="display:flex; flex-direction:column; flex-wrap:wrap; justify-content:start; gap:2px;"><div style="flex-basis:50%;"></div></div>'
);
});
}); });

@ -1,5 +1,5 @@
import { marked } from 'marked'; import { marked } from 'marked';
import { sanitize } from './sanitize'; import { sanitize, sanitizeTextPanelContent } from './sanitize';
let hasInitialized = false; let hasInitialized = false;
@ -7,15 +7,17 @@ export interface RenderMarkdownOptions {
noSanitize?: boolean; noSanitize?: boolean;
} }
const markdownOptions = {
pedantic: false,
gfm: true,
smartLists: true,
smartypants: false,
xhtml: false,
};
export function renderMarkdown(str?: string, options?: RenderMarkdownOptions): string { export function renderMarkdown(str?: string, options?: RenderMarkdownOptions): string {
if (!hasInitialized) { if (!hasInitialized) {
marked.setOptions({ marked.setOptions({ ...markdownOptions });
pedantic: false,
gfm: true,
smartLists: true,
smartypants: false,
xhtml: false,
});
hasInitialized = true; hasInitialized = true;
} }
@ -26,3 +28,17 @@ export function renderMarkdown(str?: string, options?: RenderMarkdownOptions): s
return sanitize(html); 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);
}

@ -10,6 +10,29 @@ const sanitizeXSS = new FilterXSS({
whiteList: XSSWL, 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. * 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 { export function sanitizeUrl(url: string): string {
return braintreeSanitizeUrl(url); return braintreeSanitizeUrl(url);
} }

@ -1,7 +1,7 @@
// Libraries // Libraries
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { PanelProps, renderMarkdown, textUtil } from '@grafana/data'; import { PanelProps, renderTextPanelMarkdown, textUtil } from '@grafana/data';
// Utils // Utils
import config from 'app/core/config'; import config from 'app/core/config';
// Types // Types
@ -44,7 +44,9 @@ export class TextPanel extends PureComponent<Props, State> {
prepareMarkdown(content: string): string { prepareMarkdown(content: string): string {
// Sanitize is disabled here as we handle that after variable interpolation // 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 { interpolateAndSanitizeString(content: string): string {
@ -52,7 +54,7 @@ export class TextPanel extends PureComponent<Props, State> {
content = replaceVariables(content, {}, 'html'); content = replaceVariables(content, {}, 'html');
return config.disableSanitizeHtml ? content : textUtil.sanitize(content); return config.disableSanitizeHtml ? content : textUtil.sanitizeTextPanelContent(content);
} }
processContent(options: PanelOptions): string { processContent(options: PanelOptions): string {

@ -40,7 +40,7 @@ export const TextPanelEditor: FC<StandardEditorProps<string, any, PanelOptions>>
width={width} width={width}
showMiniMap={false} showMiniMap={false}
showLineNumbers={false} showLineNumbers={false}
height="200px" height="500px"
getSuggestions={getSuggestions} getSuggestions={getSuggestions}
/> />
); );

Loading…
Cancel
Save