mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Chore: Use DOMPurify to sanitize strings rather than js-xss (#62787)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
committed by
GitHub
parent
0e565a2e6c
commit
27e2b037ae
@@ -40,6 +40,7 @@
|
||||
"@types/d3-interpolate": "^3.0.0",
|
||||
"d3-interpolate": "3.0.1",
|
||||
"date-fns": "2.29.3",
|
||||
"dompurify": "^2.4.3",
|
||||
"eventemitter3": "5.0.0",
|
||||
"fast_array_intersect": "1.1.0",
|
||||
"history": "4.10.1",
|
||||
@@ -55,7 +56,7 @@
|
||||
"tinycolor2": "1.6.0",
|
||||
"tslib": "2.5.0",
|
||||
"uplot": "1.6.24",
|
||||
"xss": "1.0.14"
|
||||
"xss": "^1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@grafana/tsconfig": "^1.2.0-rc1",
|
||||
@@ -67,6 +68,7 @@
|
||||
"@testing-library/react": "12.1.4",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/dompurify": "^2",
|
||||
"@types/history": "4.7.11",
|
||||
"@types/jest": "29.2.3",
|
||||
"@types/jquery": "3.5.16",
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
export * from './string';
|
||||
export * from './markdown';
|
||||
export * from './text';
|
||||
import { escapeHtml, hasAnsiCodes, sanitize, sanitizeUrl, sanitizeTextPanelContent } from './sanitize';
|
||||
import {
|
||||
escapeHtml,
|
||||
hasAnsiCodes,
|
||||
sanitize,
|
||||
sanitizeUrl,
|
||||
sanitizeTextPanelContent,
|
||||
sanitizeSVGContent,
|
||||
} from './sanitize';
|
||||
|
||||
export const textUtil = {
|
||||
escapeHtml,
|
||||
@@ -9,4 +16,5 @@ export const textUtil = {
|
||||
sanitize,
|
||||
sanitizeTextPanelContent,
|
||||
sanitizeUrl,
|
||||
sanitizeSVGContent,
|
||||
};
|
||||
|
||||
@@ -11,6 +11,18 @@ describe('Markdown wrapper', () => {
|
||||
expect(str).toBe('<script>alert()</script>');
|
||||
});
|
||||
|
||||
it('should only escape (and not remove) code blocks inside markdown', () => {
|
||||
const inlineCodeBlock = renderMarkdown('This is a piece of code block: `<script>alert()</script>`');
|
||||
expect(inlineCodeBlock.trim()).toBe(
|
||||
'<p>This is a piece of code block: <code><script>alert()</script></code></p>'
|
||||
);
|
||||
|
||||
const multilineCodeBlock = renderMarkdown('This is a piece of code block: ```<script>alert()</script>```');
|
||||
expect(multilineCodeBlock.trim()).toBe(
|
||||
'<p>This is a piece of code block: <code><script>alert()</script></code></p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should sanitize content in text panel by default', () => {
|
||||
const str = renderTextPanelMarkdown('<script>alert()</script>');
|
||||
expect(str).toBe('<script>alert()</script>');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { marked } from 'marked';
|
||||
|
||||
import { sanitize, sanitizeTextPanelContent } from './sanitize';
|
||||
import { sanitizeTextPanelContent } from './sanitize';
|
||||
|
||||
let hasInitialized = false;
|
||||
|
||||
@@ -37,7 +37,7 @@ export function renderMarkdown(str?: string, options?: RenderMarkdownOptions): s
|
||||
return html;
|
||||
}
|
||||
|
||||
return sanitize(html);
|
||||
return sanitizeTextPanelContent(html);
|
||||
}
|
||||
|
||||
export function renderTextPanelMarkdown(str?: string, options?: RenderMarkdownOptions): string {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sanitizeTextPanelContent } from './sanitize';
|
||||
import { sanitizeTextPanelContent, sanitizeUrl, sanitize } from './sanitize';
|
||||
|
||||
describe('Sanitize wrapper', () => {
|
||||
it('should allow whitelisted styles in text panel', () => {
|
||||
@@ -10,3 +10,20 @@ describe('Sanitize wrapper', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeUrl', () => {
|
||||
it('sanitize javascript urls', () => {
|
||||
const url = 'javascript:alert(document.domain)';
|
||||
const str = sanitizeUrl(url);
|
||||
expect(str).toBe('about:blank');
|
||||
});
|
||||
});
|
||||
|
||||
// write test to sanitize xss payloads using the sanitize function
|
||||
describe('sanitize', () => {
|
||||
it('should sanitize xss payload', () => {
|
||||
const html = '<script>alert(1)</script>';
|
||||
const str = sanitize(html);
|
||||
expect(str).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { sanitizeUrl as braintreeSanitizeUrl } from '@braintree/sanitize-url';
|
||||
import DOMPurify from 'dompurify';
|
||||
import * as xss from 'xss';
|
||||
|
||||
const XSSWL = Object.keys(xss.whiteList).reduce<xss.IWhiteList>((acc, element) => {
|
||||
@@ -6,10 +7,6 @@ const XSSWL = Object.keys(xss.whiteList).reduce<xss.IWhiteList>((acc, element) =
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const sanitizeXSS = new xss.FilterXSS({
|
||||
whiteList: XSSWL,
|
||||
});
|
||||
|
||||
const sanitizeTextPanelWhitelist = new xss.FilterXSS({
|
||||
whiteList: XSSWL,
|
||||
css: {
|
||||
@@ -34,21 +31,29 @@ const sanitizeTextPanelWhitelist = new xss.FilterXSS({
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns string safe from XSS attacks.
|
||||
* Return a sanitized string that is going to be rendered in the browser to prevent XSS attacks.
|
||||
* Note that sanitized tags will be removed, such as "<script>".
|
||||
* We don't allow form, pre, or input elements.
|
||||
*/
|
||||
export function sanitize(unsanitizedString: string): string {
|
||||
try {
|
||||
return DOMPurify.sanitize(unsanitizedString, {
|
||||
USE_PROFILES: { html: true },
|
||||
FORBID_TAGS: ['form', 'input', 'pre'],
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('String could not be sanitized', unsanitizedString);
|
||||
return escapeHtml(unsanitizedString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string safe from XSS attacks to be used in the Text panel plugin.
|
||||
*
|
||||
* Even though we allow the style-attribute, there's still default filtering applied to it
|
||||
* Info: https://github.com/leizongmin/js-xss#customize-css-filter
|
||||
* Whitelist: https://github.com/leizongmin/js-css-filter/blob/master/lib/default.js
|
||||
*/
|
||||
export function sanitize(unsanitizedString: string): string {
|
||||
try {
|
||||
return sanitizeXSS.process(unsanitizedString);
|
||||
} catch (error) {
|
||||
console.error('String could not be sanitized', unsanitizedString);
|
||||
return unsanitizedString;
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeTextPanelContent(unsanitizedString: string): string {
|
||||
try {
|
||||
return sanitizeTextPanelWhitelist.process(unsanitizedString);
|
||||
@@ -58,14 +63,28 @@ export function sanitizeTextPanelContent(unsanitizedString: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Returns sanitized SVG, free from XSS attacks to be used when rendering SVG content.
|
||||
export function sanitizeSVGContent(unsanitizedString: string): string {
|
||||
return DOMPurify.sanitize(unsanitizedString, { USE_PROFILES: { svg: true, svgFilters: true } });
|
||||
}
|
||||
|
||||
// Return a sanitized URL, free from XSS attacks, such as javascript:alert(1)
|
||||
export function sanitizeUrl(url: string): string {
|
||||
return braintreeSanitizeUrl(url);
|
||||
}
|
||||
|
||||
// Returns true if the string contains ANSI color codes.
|
||||
export function hasAnsiCodes(input: string): boolean {
|
||||
return /\u001b\[\d{1,2}m/.test(input);
|
||||
}
|
||||
|
||||
// Returns a string with HTML entities escaped.
|
||||
export function escapeHtml(str: string): string {
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\//g, '/')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user