Chore: Use DOMPurify to sanitize strings rather than js-xss (#62787)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Kristian Bremberg
2023-03-16 18:13:34 +01:00
committed by GitHub
parent 0e565a2e6c
commit 27e2b037ae
12 changed files with 93 additions and 35 deletions

View File

@@ -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",

View File

@@ -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,
};

View File

@@ -11,6 +11,18 @@ describe('Markdown wrapper', () => {
expect(str).toBe('&lt;script&gt;alert()&lt;/script&gt;');
});
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>&lt;script&gt;alert()&lt;/script&gt;</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>&lt;script&gt;alert()&lt;/script&gt;</code></p>'
);
});
it('should sanitize content in text panel by default', () => {
const str = renderTextPanelMarkdown('<script>alert()</script>');
expect(str).toBe('&lt;script&gt;alert()&lt;/script&gt;');

View File

@@ -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 {

View File

@@ -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('');
});
});

View File

@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&#39;')
.replace(/\//g, '&#47;')
.replace(/"/g, '&quot;');
}