diff options
author | David Cho-Lerat <david.cho-lerat@sonarsource.com> | 2024-10-03 10:09:00 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-10-08 20:02:47 +0000 |
commit | c9ecf7fa2f9af18f474a2aea2c7c3cefd3a6e80f (patch) | |
tree | 40b9f0694c1bd3baaee84ade9fa4059b851c7ae2 | |
parent | 12664adb48e6b95e521b0e05d81f916af2a88bc4 (diff) | |
download | sonarqube-c9ecf7fa2f9af18f474a2aea2c7c3cefd3a6e80f.tar.gz sonarqube-c9ecf7fa2f9af18f474a2aea2c7c3cefd3a6e80f.zip |
SONAR-23249 Fix SSF-656 & SSF-657
30 files changed, 652 insertions, 355 deletions
diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc index 49071ab4dbf..7da42df461d 100644 --- a/server/sonar-web/.eslintrc +++ b/server/sonar-web/.eslintrc @@ -35,6 +35,28 @@ ], 1 ], + "react/forbid-component-props": [ + "error", + { + "forbid": [ + { + "propName": "dangerouslySetInnerHTML", + "message": "Use the SafeHTMLInjection component instead of 'dangerouslySetInnerHTML', to prevent CSS injection along other XSS attacks" + } + ] + } + ], + "react/forbid-dom-props": [ + "error", + { + "forbid": [ + { + "propName": "dangerouslySetInnerHTML", + "message": "Use the SafeHTMLInjection component instead of 'dangerouslySetInnerHTML', to prevent CSS injection along other XSS attacks" + } + ] + } + ], "react/forbid-elements": [ "warn", { diff --git a/server/sonar-web/design-system/.eslintrc b/server/sonar-web/design-system/.eslintrc index b6218abddcd..01e7937531f 100644 --- a/server/sonar-web/design-system/.eslintrc +++ b/server/sonar-web/design-system/.eslintrc @@ -5,6 +5,28 @@ "rules": { // Custom SonarCloud config that differs from eslint-config-sonarqube "camelcase": "off", + "react/forbid-component-props": [ + "error", + { + "forbid": [ + { + "propName": "dangerouslySetInnerHTML", + "message": "Use the SafeHTMLInjection component instead of 'dangerouslySetInnerHTML', to prevent CSS injection along other XSS attacks" + } + ] + } + ], + "react/forbid-dom-props": [ + "error", + { + "forbid": [ + { + "propName": "dangerouslySetInnerHTML", + "message": "Use the SafeHTMLInjection component instead of 'dangerouslySetInnerHTML', to prevent CSS injection along other XSS attacks" + } + ] + } + ], "react/jsx-sort-props": "error", "react/jsx-pascal-case": [2, { "allowNamespace": true }], "react/jsx-no-constructed-context-values": "error", diff --git a/server/sonar-web/design-system/package.json b/server/sonar-web/design-system/package.json index 866433a8085..2e459a6c9ee 100644 --- a/server/sonar-web/design-system/package.json +++ b/server/sonar-web/design-system/package.json @@ -72,6 +72,7 @@ "d3-shape": "3.2.0", "d3-zoom": "3.0.0", "date-fns": "4.1.0", + "dompurify": "3.1.7", "lodash": "4.17.21", "react": "18.3.1", "react-day-picker": "8.10.0", diff --git a/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx b/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx index 57cd229e71f..693c3cdde83 100644 --- a/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx +++ b/server/sonar-web/design-system/src/components/CodeSyntaxHighlighter.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import styled from '@emotion/styled'; import classNames from 'classnames'; import hljs, { HighlightResult } from 'highlight.js'; @@ -26,6 +27,7 @@ import abap from 'highlightjs-sap-abap'; import tw from 'twin.macro'; import { themeColor, themeContrast } from '../helpers/theme'; import { hljsIssueIndicatorPlugin, hljsUnderlinePlugin } from '../sonar-aligned'; +import { SafeHTMLInjection, SanitizeLevel } from '../sonar-aligned/helpers/sanitize'; hljs.registerLanguage('abap', abap); hljs.registerLanguage('apex', apex); @@ -47,6 +49,7 @@ interface Props { escapeDom?: boolean; htmlAsString: string; language?: string; + sanitizeLevel?: SanitizeLevel; wrap?: boolean | 'words'; } @@ -60,8 +63,15 @@ const htmlDecode = (escapedCode: string) => { return doc.documentElement.textContent ?? ''; }; -export function CodeSyntaxHighlighter(props: Props) { - const { className, htmlAsString, language, wrap, escapeDom = true } = props; +export function CodeSyntaxHighlighter(props: Readonly<Props>) { + const { + className, + escapeDom = true, + htmlAsString, + language, + sanitizeLevel = SanitizeLevel.FORBID_STYLE, + wrap, + } = props; let highlightedHtmlAsString = htmlAsString; htmlAsString.match(GLOBAL_REGEXP)?.forEach((codeBlock) => { @@ -95,16 +105,15 @@ export function CodeSyntaxHighlighter(props: Props) { }); return ( - <StyledSpan - className={classNames( - `hljs ${className ?? ''}`, - { 'code-wrap': wrap }, - { 'wrap-words': wrap === 'words' }, - )} - // Safe: value is escaped by highlight.js - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: highlightedHtmlAsString }} - /> + <SafeHTMLInjection htmlAsString={highlightedHtmlAsString} sanitizeLevel={sanitizeLevel}> + <StyledSpan + className={classNames( + `hljs ${className ?? ''}`, + { 'code-wrap': wrap }, + { 'wrap-words': wrap === 'words' }, + )} + /> + </SafeHTMLInjection> ); } diff --git a/server/sonar-web/design-system/src/components/Text.tsx b/server/sonar-web/design-system/src/components/Text.tsx index 08437dcce22..25df5241e02 100644 --- a/server/sonar-web/design-system/src/components/Text.tsx +++ b/server/sonar-web/design-system/src/components/Text.tsx @@ -17,10 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import styled from '@emotion/styled'; import { ElementType } from 'react'; import tw from 'twin.macro'; import { themeColor, themeContrast } from '../helpers/theme'; +import { SafeHTMLInjection } from '../sonar-aligned/helpers/sanitize'; interface TextBoldProps { className?: string; @@ -32,12 +34,9 @@ interface TextBoldProps { */ export function TextBold({ match, name, className }: TextBoldProps) { return match ? ( - <StyledText - className={className} - // Safe: comes from the search engine, that injects bold tags into component names - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: match }} - /> + <SafeHTMLInjection htmlAsString={match}> + <StyledText className={className} /> + </SafeHTMLInjection> ) : ( <StyledText className={className} title={name}> {name} @@ -47,7 +46,7 @@ export function TextBold({ match, name, className }: TextBoldProps) { /** @deprecated Use Text (with `isSubdued` prop) from Echoes instead. */ -export function TextMuted({ text, className }: { className?: string; text: string }) { +export function TextMuted({ text, className }: Readonly<{ className?: string; text: string }>) { return ( <StyledMutedText className={className} title={text}> {text} diff --git a/server/sonar-web/design-system/src/helpers/index.ts b/server/sonar-web/design-system/src/helpers/index.ts index 73dd1464b22..7af0536ce90 100644 --- a/server/sonar-web/design-system/src/helpers/index.ts +++ b/server/sonar-web/design-system/src/helpers/index.ts @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + export * from './colors'; export * from './constants'; export * from './keyboard'; diff --git a/server/sonar-web/design-system/src/sonar-aligned/helpers/__tests__/sanitize-test.tsx b/server/sonar-web/design-system/src/sonar-aligned/helpers/__tests__/sanitize-test.tsx new file mode 100644 index 00000000000..b8be9b99daf --- /dev/null +++ b/server/sonar-web/design-system/src/sonar-aligned/helpers/__tests__/sanitize-test.tsx @@ -0,0 +1,294 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { render, screen } from '@testing-library/react'; +import { + SafeHTMLInjection, + sanitizeHTMLNoSVGNoMathML, + sanitizeHTMLRestricted, + sanitizeHTMLToPreventCSSInjection, + sanitizeHTMLUserInput, +} from '../sanitize'; + +/* + * Test code borrowed from OWASP's sanitizer tests + * https://github.com/OWASP/java-html-sanitizer/blob/master/src/test/resources/org/owasp/html/htmllexerinput1.html + */ +const tainted = `<?xml version="not-even-close"?> + +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> + +<!-- a test input for HtmlLexer --> + +<html lang="en" xml:lang="en"> +<head> +<title>Test File For HtmlLexer & HtmlParser</title> +<link rel=stylesheet type="text/css" src=foo/bar.css /> +<body + bgcolor=white + linkcolor = "blue" + onload="document.writeln( + "<p>properly escaped code in a handler</p>");" +> + +<script type="text/javascript"><!-- +document.writeln("<p>Some initialization code in global context</p>"); +--></script> + +<script type="text/javascript"> +// hi there +document.writeln("<p>More initialization</p>"); +</script> + +<div id=clickydiv onclick="handleClicky(event)" + ondblclick=this.onclick(event);return(false)> +Clicky +</div> + +<input id=foo> +<gxp:attr name="onchange">alert("<b>hi</b>");</gxp:attr> +</input> + +<pre><div id=notarealtag onclick=notcode()></pre> + +<!-- some tokenization corner cases --> + +< notatag <atag/> + +</ notatag> </redundantlyclosed/> + +<messyattributes a=b=c d="e"f=g h =i j= k l = m checked n="o"/> + +< < < all in one text block > > > + +<xmp>Make sure that <!-- comments don't obscure the xmp close</xmp> +<% # some php code here +write("<pre>$horriblySyntacticConstruct1</pre>\n\n"); +%> +<script type="text/javascript"><!-- +alert("hello world"); +// --></script> + +<script>/* </script> */alert('hi');</script> +<script><!--/* </script> */alert('hi');--></script> + +<xmp style=color:blue><!--/* </xmp> */alert('hi');--></xmp> + +<style><!-- p { contentf: '</style>' } --></style> +<style>Foo<!-- > </style> --></style> +<textarea><!-- Zoicks </textarea>--></textarea> +<!-- An escaping text span start may share its U+002D HYPHEN-MINUS characters + - with its corresponding escaping text span end. --> +<script><!--></script> +<script><!---></script> +<script><!----></script> + +This is <b>bold</b> and this is <i>italic</i> and this is <u>underlined</u>. +<br /> +A <blockquote>quote</blockquote> and a <code>code</code> and a <pre>pre</pre>. +An <h1>h1</h1> and an <h2>h2</h2> and an <h3>h3</h3> and an <h4>h4</h4> and an <h5>h5</h5> and an <h6>h6</h6>. +An <ol><li>ol</li></ol> and a <ul><li>ul</li></ul> and a <p style="color:blue">p</p>. +A <strong>strong</strong> and a <a href="foo" ping="pong" rel="noopener" target="__blank" >link</a> + +<a href="javascript:alert('hello')" target="_blank">this is wrong</a> + +<svg><text>SVG isn't always allowed</text></svg> + +<math xmlns="http://www.w3.org/1998/Math/MathML"> + <infinity /> +</math> + +</body> +</html> +<![CDATA[ No such thing as a CDATA> section in HTML ]]> +<script>a<b</script> +<img src=foo.gif /><a href=><a href=/> +<span title=malformed attribs' do=don't id=foo checked onclick="a<b">Bar</span>`; + +describe('sanitizeHTMLToPreventCSSInjection', () => { + it('should strip off style attributes', () => { + const clean = ` + <div id="clickydiv"> + Clicky + </div> + <input id="foo"> + alert("<b>hi</b>"); + <pre><div id=notarealtag onclick=notcode()></pre> + < notatag + < < < all in one text block > > > + <% # some php code here + write("<pre>$horriblySyntacticConstruct1</pre> + "); + %> + */alert('hi'); + */alert('hi');--> + */alert('hi');--> ' } --> + --> + <textarea><!-- Zoicks </textarea>--> + This is <b>bold</b> and this is <i>italic</i> and this is <u>underlined</u>. + <br> + A <blockquote>quote</blockquote> and a <code>code</code> and a <pre>pre</pre>. + An <h1>h1</h1> and an <h2>h2</h2> and an <h3>h3</h3> and an <h4>h4</h4> and an <h5>h5</h5> and an <h6>h6</h6>. + An <ol><li>ol</li></ol> and a <ul><li>ul</li></ul> and a <p>p</p>. + A <strong>strong</strong> and a <a rel="noopener" href="foo">link</a> + <a>this is wrong</a> + <svg><text>SVG isn't always allowed</text></svg> + <math xmlns="http://www.w3.org/1998/Math/MathML"> + </math> + section in HTML ]]> + <img src="foo.gif"><a href=""></a><a href="/"> + <span checked="" id="foo" title="malformed">Bar</span></a>`; + + expect(sanitizeHTMLToPreventCSSInjection(tainted).trimEnd().replace(/\s+/g, ' ')).toBe( + clean.replace(/\s+/g, ' '), + ); + }); +}); + +describe('sanitizeHTMLNoSVGNoMathML', () => { + it('should not allow MathML and SVG', () => { + const clean = ` + <div id="clickydiv"> + Clicky + </div> + <input id="foo"> + alert("<b>hi</b>"); + <pre><div id=notarealtag onclick=notcode()></pre> + < notatag + < < < all in one text block > > > + <% # some php code here + write("<pre>$horriblySyntacticConstruct1</pre> + "); + %> + */alert('hi'); + */alert('hi');--> + */alert('hi');--> ' } --> + --> + <textarea><!-- Zoicks </textarea>--> + This is <b>bold</b> and this is <i>italic</i> and this is <u>underlined</u>. + <br> + A <blockquote>quote</blockquote> and a <code>code</code> and a <pre>pre</pre>. + An <h1>h1</h1> and an <h2>h2</h2> and an <h3>h3</h3> and an <h4>h4</h4> and an <h5>h5</h5> and an <h6>h6</h6>. + An <ol><li>ol</li></ol> and a <ul><li>ul</li></ul> and a <p>p</p>. + A <strong>strong</strong> and a <a rel="noopener" href="foo">link</a> + <a>this is wrong</a> + section in HTML ]]> + <img src="foo.gif"><a href=""></a><a href="/"> + <span checked="" id="foo" title="malformed">Bar</span></a>`; + + expect(sanitizeHTMLNoSVGNoMathML(tainted).trimEnd().replace(/\s+/g, ' ')).toBe( + clean.replace(/\s+/g, ' '), + ); + }); +}); + +describe('sanitizeHTMLUserInput', () => { + it('should preserve only specific formatting tags and attributes', () => { + const clean = ` + Clicky + alert("<b>hi</b>"); + <pre><div id=notarealtag onclick=notcode()></pre> + < notatag + < < < all in one text block > > > + <% # some php code here + write("<pre>$horriblySyntacticConstruct1</pre> + "); + %> + */alert('hi'); + */alert('hi');--> + */alert('hi');--> ' } --> + --> + <!-- Zoicks --> + This is <b>bold</b> and this is <i>italic</i> and this is underlined. + <br> + A <blockquote>quote</blockquote> and a <code>code</code> and a <pre>pre</pre>. + An <h1>h1</h1> and an <h2>h2</h2> and an <h3>h3</h3> and an <h4>h4</h4> and an <h5>h5</h5> and an <h6>h6</h6>. + An <ol><li>ol</li></ol> and a <ul><li>ul</li></ul> and a <p>p</p>. + A <strong>strong</strong> and a <a rel="noopener" href="foo">link</a> + <a>this is wrong</a> + section in HTML ]]> + <a href=""></a><a href="/"> + Bar</a>`; + + expect(sanitizeHTMLUserInput(tainted).trimEnd().replace(/\s+/g, ' ')).toBe( + clean.replace(/\s+/g, ' '), + ); + }); +}); + +describe('sanitizeHTMLRestricted', () => { + it('should preserve only a very limited list of formatting tags and attributes', () => { + const clean = ` + Clicky + alert("<b>hi</b>"); + <div id=notarealtag onclick=notcode()> + < notatag + < < < all in one text block > > > + <% # some php code here + write("$horriblySyntacticConstruct1 + "); + %> + */alert('hi'); + */alert('hi');--> + */alert('hi');--> ' } --> + --> + <!-- Zoicks --> + This is <b>bold</b> and this is <i>italic</i> and this is underlined. + <br> + A quote and a <code>code</code> and a pre. + An h1 and an h2 and an h3 and an h4 and an h5 and an h6. + An <li>ol</li> and a <ul><li>ul</li></ul> and a <p>p</p>. + A <strong>strong</strong> and a <a href="foo">link</a> + <a>this is wrong</a> + section in HTML ]]> + <a href=""></a><a href="/"> + Bar</a>`; + + expect(sanitizeHTMLRestricted(tainted).trimEnd().replace(/\s+/g, ' ')).toBe( + clean.replace(/\s+/g, ' '), + ); + }); +}); + +describe('SafeHTMLInjection', () => { + it('should default to a span and the SanitizeLevel.FORBID_STYLE level', () => { + const tainted = ` + <head> + <link rel=stylesheet type="text/css" src=foo/bar.css /> + <style>some style</style> + </head> + + <body> + <p style="color:blue">a stylish paragraph</p> + + <svg><text>SVG isn't always allowed</text></svg> + + <math xmlns="http://www.w3.org/1998/Math/MathML"> + <infinity /> + </math> + </body> + `; + + render(<SafeHTMLInjection htmlAsString={tainted} />); + + expect(screen.getByText('a stylish paragraph')).toBeInTheDocument(); + expect(screen.getByText("SVG isn't always allowed")).toBeInTheDocument(); + }); +}); diff --git a/server/sonar-web/design-system/src/sonar-aligned/helpers/index.ts b/server/sonar-web/design-system/src/sonar-aligned/helpers/index.ts index 863797b2d1f..e288f05e8ee 100644 --- a/server/sonar-web/design-system/src/sonar-aligned/helpers/index.ts +++ b/server/sonar-web/design-system/src/sonar-aligned/helpers/index.ts @@ -18,4 +18,5 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ +export * from './sanitize'; export * from './tabs'; diff --git a/server/sonar-web/design-system/src/sonar-aligned/helpers/sanitize.tsx b/server/sonar-web/design-system/src/sonar-aligned/helpers/sanitize.tsx new file mode 100644 index 00000000000..1426c567efd --- /dev/null +++ b/server/sonar-web/design-system/src/sonar-aligned/helpers/sanitize.tsx @@ -0,0 +1,124 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import dompurify from 'dompurify'; + +import React from 'react'; + +const { sanitize } = dompurify; + +export enum SanitizeLevel { + FORBID_STYLE, // minimum sanitation level to prevent CSS injections + FORBID_SVG_MATHML, // adds SVG and MathML exclusion + USER_INPUT, // adds restrictions on tags and attributes + RESTRICTED, // adds even more restrictions on tags and attributes +} + +export const sanitizeFunctionByLevel = (sanitizeLevel: SanitizeLevel) => + ({ + [SanitizeLevel.FORBID_STYLE]: sanitizeHTMLToPreventCSSInjection, + [SanitizeLevel.FORBID_SVG_MATHML]: sanitizeHTMLNoSVGNoMathML, + [SanitizeLevel.USER_INPUT]: sanitizeHTMLUserInput, + [SanitizeLevel.RESTRICTED]: sanitizeHTMLRestricted, + })[sanitizeLevel]; + +export const sanitizeHTMLToPreventCSSInjection = (htmlAsString: string) => + sanitize(htmlAsString, { + FORBID_ATTR: ['style'], + FORBID_TAGS: ['style'], + }); + +export function sanitizeHTMLNoSVGNoMathML(htmlAsString: string) { + return sanitize(htmlAsString, { + FORBID_ATTR: ['style'], + FORBID_TAGS: ['style'], + USE_PROFILES: { html: true }, + }); +} + +export function sanitizeHTMLUserInput(htmlAsString: string) { + return sanitize(htmlAsString, { + ALLOWED_ATTR: ['href', 'rel'], + ALLOWED_TAGS: [ + 'a', + 'b', + 'blockquote', + 'br', + 'code', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'i', + 'li', + 'ol', + 'p', + 'pre', + 'strong', + 'ul', + ], + }); +} + +export function sanitizeHTMLRestricted(htmlAsString: string) { + return sanitize(htmlAsString, { + ALLOWED_ATTR: ['href'], + ALLOWED_TAGS: ['a', 'b', 'br', 'code', 'i', 'li', 'p', 'strong', 'ul'], + }); +} + +/** + * Safely injects HTML into an element with no risk of XSS attacks. + * + * @param children The React element to clone with the sanitized HTML (defaults to a `span`) + * @param htmlAsString The HTML string to sanitize and inject (required) + * @param sanitizeLevel The level of sanitation to apply (defaults to `SanitizeLevel.FORBID_STYLE`) + * + * @returns A React element with the sanitized HTML injected, and all other props preserved + * + * @example + * Here's a simple example with no children: + * ``` + * <SafeHTMLInjection htmlAsString={taintedString} /> + * ``` + * + * @example + * Here's an example with a custom `sanitizeLevel` and a child `div`: + * ``` + * <SafeHTMLInjection htmlAsString={taintedString} sanitizeLevel={SanitizeLevel.RESTRICTED}> + * // the HTML will be safely injected in the div below, with the className preserved: + * <div className="someClassThatWillBePreserved" /> + * </SafeHTMLInjection> + * ``` + */ +export const SafeHTMLInjection = ({ + children, + htmlAsString, + sanitizeLevel = SanitizeLevel.FORBID_STYLE, +}: Readonly<{ + children?: React.ReactElement; + htmlAsString: string; + sanitizeLevel?: SanitizeLevel; +}>) => + React.cloneElement(children ?? <span />, { + dangerouslySetInnerHTML: { __html: sanitizeFunctionByLevel(sanitizeLevel)(htmlAsString) }, + }); diff --git a/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx index 3671ee2f0f8..c6134af333c 100644 --- a/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx +++ b/server/sonar-web/src/main/js/apps/background-tasks/components/AnalysisWarningsModal.tsx @@ -17,13 +17,20 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { Button, ButtonVariety } from '@sonarsource/echoes-react'; -import { FlagMessage, HtmlFormatter, Modal, Spinner } from 'design-system'; +import { + FlagMessage, + HtmlFormatter, + Modal, + SafeHTMLInjection, + SanitizeLevel, + Spinner, +} from 'design-system'; import * as React from 'react'; import { dismissAnalysisWarning, getTask } from '../../../api/ce'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import { translate } from '../../../helpers/l10n'; -import { sanitizeStringRestricted } from '../../../helpers/sanitize'; import { TaskWarning } from '../../../types/tasks'; import { CurrentUser } from '../../../types/users'; @@ -118,11 +125,9 @@ export class AnalysisWarningsModal extends React.PureComponent<Props, State> { <div className="sw-flex sw-items-center sw-mt-2"> <FlagMessage variant="warning"> <HtmlFormatter> - <span - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ - __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')), - }} + <SafeHTMLInjection + htmlAsString={message.trim().replace(/\n/g, '<br />')} + sanitizeLevel={SanitizeLevel.RESTRICTED} /> </HtmlFormatter> </FlagMessage> diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx index 7aa212f1358..efa190f33f0 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx @@ -27,6 +27,8 @@ import { InputTextArea, LabelValueSelectOption, Note, + SafeHTMLInjection, + SanitizeLevel, Switch, } from 'design-system'; import * as React from 'react'; @@ -35,7 +37,6 @@ import { Profile } from '../../../api/quality-profiles'; import { useAvailableFeatures } from '../../../app/components/available-features/withAvailableFeatures'; import DocumentationLink from '../../../components/common/DocumentationLink'; import { DocLink } from '../../../helpers/doc-links'; -import { sanitizeString } from '../../../helpers/sanitize'; import { useActivateRuleMutation } from '../../../queries/quality-profiles'; import { Feature } from '../../../types/features'; import { IssueSeverity } from '../../../types/issues'; @@ -265,11 +266,12 @@ export default function ActivationFormModal(props: Readonly<Props>) { /> )} {param.htmlDesc !== undefined && ( - <Note - as="div" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }} - /> + <SafeHTMLInjection + htmlAsString={param.htmlDesc} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <Note as="div" /> + </SafeHTMLInjection> )} </FormField> )) diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx index 4f7142ab3a9..2c6fb0cd8e6 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { HttpStatusCode } from 'axios'; import { ButtonPrimary, @@ -28,6 +29,8 @@ import { LabelValueSelectOption, LightLabel, Modal, + SafeHTMLInjection, + SanitizeLevel, } from 'design-system'; import * as React from 'react'; import { Status } from '~sonar-aligned/types/common'; @@ -36,7 +39,6 @@ import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsEx import { RULE_STATUSES } from '../../../helpers/constants'; import { csvEscape } from '../../../helpers/csv'; import { translate } from '../../../helpers/l10n'; -import { sanitizeString } from '../../../helpers/sanitize'; import { latinize } from '../../../helpers/strings'; import { useCreateRuleMutation, useUpdateRuleMutation } from '../../../queries/rules'; import { @@ -294,11 +296,14 @@ export default function CustomRuleFormModal(props: Readonly<Props>) { value={actualValue} /> )} + {param.htmlDesc !== undefined && ( - <LightLabel - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }} - /> + <SafeHTMLInjection + htmlAsString={param.htmlDesc} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <LightLabel /> + </SafeHTMLInjection> )} </FormField> ); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx index 0a9ac8ee0d4..5b1dffbbc25 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx @@ -23,13 +23,13 @@ import { ButtonSecondary, CodeSyntaxHighlighter, InputTextArea, + SanitizeLevel, } from 'design-system'; import * as React from 'react'; import FormattingTips from '../../../components/common/FormattingTips'; import RuleTabViewer from '../../../components/rules/RuleTabViewer'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../helpers/sanitize'; import { useUpdateRuleMutation } from '../../../queries/rules'; import { RuleDetails } from '../../../types/types'; import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal'; @@ -61,8 +61,9 @@ export default function RuleDetailsDescription(props: Readonly<Props>) { {ruleDetails.htmlNote !== undefined && ( <CodeSyntaxHighlighter className="markdown sw-my-6" - htmlAsString={sanitizeUserInput(ruleDetails.htmlNote)} + htmlAsString={ruleDetails.htmlNote} language={ruleDetails.lang} + sanitizeLevel={SanitizeLevel.USER_INPUT} /> )} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx index fc7a6486c66..316e72a51aa 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx @@ -17,10 +17,18 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { CellComponent, Note, SubHeadingHighlight, Table, TableRow } from 'design-system'; + +import { + CellComponent, + Note, + SafeHTMLInjection, + SanitizeLevel, + SubHeadingHighlight, + Table, + TableRow, +} from 'design-system'; import * as React from 'react'; import { translate } from '../../../helpers/l10n'; -import { sanitizeString } from '../../../helpers/sanitize'; import { RuleParameter } from '../../../types/types'; interface Props { @@ -38,10 +46,12 @@ export default function RuleDetailsParameters({ params }: Props) { <CellComponent> <div className="sw-flex sw-flex-col sw-gap-2"> {param.htmlDesc !== undefined && ( - <div - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }} - /> + <SafeHTMLInjection + htmlAsString={param.htmlDesc} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <div /> + </SafeHTMLInjection> )} {param.defaultValue !== undefined && ( <Note as="div"> diff --git a/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx b/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx index df464f021d5..cc7b2e74d1b 100644 --- a/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx +++ b/server/sonar-web/src/main/js/apps/issues/components/IssueReviewHistory.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import styled from '@emotion/styled'; import { DangerButtonPrimary, @@ -26,6 +27,8 @@ import { LightLabel, Modal, PencilIcon, + SafeHTMLInjection, + SanitizeLevel, TrashIcon, themeBorder, } from 'design-system'; @@ -35,7 +38,6 @@ import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import IssueChangelogDiff from '../../../components/issue/components/IssueChangelogDiff'; import Avatar from '../../../components/ui/Avatar'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../helpers/sanitize'; import { ReviewHistoryType } from '../../../types/security-hotspots'; import { Issue, IssueChangelog } from '../../../types/types'; import HotspotCommentModal from '../../security-hotspots/components/HotspotCommentModal'; @@ -47,7 +49,20 @@ export interface HotspotReviewHistoryProps { onEditComment: (key: string, comment: string) => void; } -export default function IssueReviewHistory(props: HotspotReviewHistoryProps) { +const getUpdatedChangelog = ({ changelog }: { changelog: IssueChangelog[] }) => + changelog.map((changelogItem) => { + const diffHasIssueStatusChange = changelogItem.diffs.some((diff) => diff.key === 'issueStatus'); + + return { + ...changelogItem, + // If the diff is an issue status change, we remove deprecated status and resolution diffs + diffs: changelogItem.diffs.filter( + (diff) => !(diffHasIssueStatusChange && ['resolution', 'status'].includes(diff.key)), + ), + }; + }); + +export default function IssueReviewHistory(props: Readonly<HotspotReviewHistoryProps>) { const { issue } = props; const [changeLog, setChangeLog] = React.useState<IssueChangelog[]>([]); const history = useGetIssueReviewHistory(issue, changeLog); @@ -57,19 +72,8 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) { React.useEffect(() => { getIssueChangelog(issue.key).then( ({ changelog }) => { - const updatedChangelog = changelog.map((changelogItem) => { - const diffHasIssueStatusChange = changelogItem.diffs.some( - (diff) => diff.key === 'issueStatus', - ); - - return { - ...changelogItem, - // If the diff is an issue status change, we remove deprecated status and resolution diffs - diffs: changelogItem.diffs.filter( - (diff) => !(diffHasIssueStatusChange && ['resolution', 'status'].includes(diff.key)), - ), - }; - }); + const updatedChangelog = getUpdatedChangelog({ changelog }); + setChangeLog(updatedChangelog); }, () => {}, @@ -85,6 +89,7 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) { <div className="sw-typo-semibold sw-mb-1"> <DateTimeFormatter date={date} /> </div> + <LightLabel as="div" className="sw-flex sw-gap-2"> {user.name && ( <div className="sw-flex sw-items-center sw-gap-1"> @@ -112,11 +117,9 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) { {type === ReviewHistoryType.Comment && key && html && markdown && ( <div className="sw-mt-2 sw-flex sw-justify-between"> - <CommentBox - className="sw-pl-2 sw-ml-2 sw-typo-default" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeUserInput(html) }} - /> + <SafeHTMLInjection htmlAsString={html} sanitizeLevel={SanitizeLevel.USER_INPUT}> + <CommentBox className="sw-pl-2 sw-ml-2 sw-typo-default" /> + </SafeHTMLInjection> {updatable && ( <div className="sw-flex sw-gap-6"> @@ -127,6 +130,7 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) { size="small" stopPropagation={false} /> + <DestructiveIcon Icon={TrashIcon} aria-label={translate('issue.comment.delete')} diff --git a/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx index 5a4daac6b9e..3390089477d 100644 --- a/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx +++ b/server/sonar-web/src/main/js/apps/overview/components/AnalysisWarningsModal.tsx @@ -17,12 +17,19 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { Button, ButtonVariety } from '@sonarsource/echoes-react'; -import { FlagMessage, HtmlFormatter, Modal, Spinner } from 'design-system'; +import { + FlagMessage, + HtmlFormatter, + Modal, + SafeHTMLInjection, + SanitizeLevel, + Spinner, +} from 'design-system'; import * as React from 'react'; import withCurrentUserContext from '../../../app/components/current-user/withCurrentUserContext'; import { translate } from '../../../helpers/l10n'; -import { sanitizeStringRestricted } from '../../../helpers/sanitize'; import { useDismissBranchWarningMutation } from '../../../queries/branch'; import { TaskWarning } from '../../../types/tasks'; import { Component } from '../../../types/types'; @@ -51,11 +58,9 @@ export function AnalysisWarningsModal(props: Props) { <div className="sw-flex sw-items-center sw-mt-2"> <FlagMessage variant="warning"> <HtmlFormatter> - <span - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ - __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '<br />')), - }} + <SafeHTMLInjection + htmlAsString={message.trim().replace(/\n/g, '<br />')} + sanitizeLevel={SanitizeLevel.RESTRICTED} /> </HtmlFormatter> </FlagMessage> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx index 5b0cf3fbd3f..75a88ada80d 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx @@ -17,6 +17,7 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import styled from '@emotion/styled'; import { DangerButtonPrimary, @@ -26,6 +27,8 @@ import { LightLabel, Modal, PencilIcon, + SafeHTMLInjection, + SanitizeLevel, TrashIcon, themeBorder, } from 'design-system'; @@ -34,7 +37,6 @@ import DateTimeFormatter from '../../../components/intl/DateTimeFormatter'; import IssueChangelogDiff from '../../../components/issue/components/IssueChangelogDiff'; import Avatar from '../../../components/ui/Avatar'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../helpers/sanitize'; import { Hotspot, ReviewHistoryType } from '../../../types/security-hotspots'; import { getHotspotReviewHistory } from '../utils'; import HotspotCommentModal from './HotspotCommentModal'; @@ -45,7 +47,7 @@ export interface HotspotReviewHistoryProps { onEditComment: (key: string, comment: string) => void; } -export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) { +export default function HotspotReviewHistory(props: Readonly<HotspotReviewHistoryProps>) { const { hotspot } = props; const history = getHotspotReviewHistory(hotspot); const [editCommentKey, setEditCommentKey] = React.useState(''); @@ -86,11 +88,9 @@ export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) { {type === ReviewHistoryType.Comment && key && html && markdown && ( <div className="sw-mt-2 sw-flex sw-justify-between"> - <CommentBox - className="sw-pl-2 sw-ml-2 sw-typo-default" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeUserInput(html) }} - /> + <SafeHTMLInjection htmlAsString={html} sanitizeLevel={SanitizeLevel.USER_INPUT}> + <CommentBox className="sw-pl-2 sw-ml-2 sw-typo-default" /> + </SafeHTMLInjection> {updatable && ( <div className="sw-flex sw-gap-6"> diff --git a/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx b/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx index 21ea7d31278..1e30ae1f29e 100644 --- a/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx +++ b/server/sonar-web/src/main/js/apps/sessions/components/Login.tsx @@ -24,6 +24,8 @@ import { Card, FlagMessage, PageContentFontWrapper, + SafeHTMLInjection, + SanitizeLevel, Title, themeBorder, themeColor, @@ -33,7 +35,6 @@ import { Helmet } from 'react-helmet-async'; import { Image } from '~sonar-aligned/components/common/Image'; import { Location } from '~sonar-aligned/types/router'; import { translate } from '../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../helpers/sanitize'; import { getReturnUrl } from '../../../helpers/urls'; import { IdentityProvider } from '../../../types/types'; import LoginForm from './LoginForm'; @@ -69,11 +70,9 @@ export default function Login(props: Readonly<LoginProps>) { )} {message !== undefined && message.length > 0 && ( - <StyledMessage - className="markdown sw-rounded-2 sw-p-4 sw-mb-6" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeUserInput(message) }} - /> + <SafeHTMLInjection htmlAsString={message} sanitizeLevel={SanitizeLevel.USER_INPUT}> + <StyledMessage className="markdown sw-rounded-2 sw-p-4 sw-mb-6" /> + </SafeHTMLInjection> )} {identityProviders.length > 0 && ( diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx index b2635d566e9..89604821359 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionDescription.tsx @@ -17,11 +17,11 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { Text, Tooltip } from '@sonarsource/echoes-react'; -import { SubHeading } from 'design-system'; +import { SafeHTMLInjection, SanitizeLevel, SubHeading } from 'design-system'; import * as React from 'react'; import { translateWithParameters } from '../../../helpers/l10n'; -import { sanitizeStringRestricted } from '../../../helpers/sanitize'; import { ExtendedSettingDefinition } from '../../../types/settings'; import { getPropertyDescription, getPropertyName } from '../utils'; @@ -40,11 +40,9 @@ export default function DefinitionDescription({ definition }: Readonly<Props>) { </SubHeading> {description && ( - <div - className="markdown sw-mt-1" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeStringRestricted(description) }} - /> + <SafeHTMLInjection htmlAsString={description} sanitizeLevel={SanitizeLevel.RESTRICTED}> + <div className="markdown sw-mt-1" /> + </SafeHTMLInjection> )} <Tooltip content={translateWithParameters('settings.key_x', definition.key)}> diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx index bea02959d5f..40c4b3a8362 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx @@ -17,12 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { BasicSeparator, Note, SubTitle } from 'design-system'; + +import { BasicSeparator, Note, SafeHTMLInjection, SanitizeLevel, SubTitle } from 'design-system'; import { groupBy, sortBy } from 'lodash'; import * as React from 'react'; import { withRouter } from '~sonar-aligned/components/hoc/withRouter'; import { Location } from '~sonar-aligned/types/router'; -import { sanitizeStringRestricted } from '../../../helpers/sanitize'; import { SettingDefinitionAndValue } from '../../../types/settings'; import { Component } from '../../../types/types'; import { SUB_CATEGORY_EXCLUSIONS } from '../constants'; @@ -93,15 +93,16 @@ class SubCategoryDefinitionsList extends React.PureComponent<SubCategoryDefiniti {subCategory.name} </SubTitle> )} + {subCategory.description != null && ( - <Note - className="markdown" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ - __html: sanitizeStringRestricted(subCategory.description), - }} - /> + <SafeHTMLInjection + htmlAsString={subCategory.description} + sanitizeLevel={SanitizeLevel.RESTRICTED} + > + <Note className="markdown" /> + </SafeHTMLInjection> )} + <BasicSeparator className="sw-mt-6" /> <DefinitionsList component={component} diff --git a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx index d2372b36a5f..d57d717c4fb 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx @@ -24,13 +24,14 @@ import { HtmlFormatter, InputTextArea, PencilIcon, + SafeHTMLInjection, + SanitizeLevel, themeBorder, themeColor, } from 'design-system'; import * as React from 'react'; import FormattingTipsWithLink from '../../../../components/common/FormattingTipsWithLink'; import { translate } from '../../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../../helpers/sanitize'; import { DefaultSpecializedInputProps, getPropertyName } from '../../utils'; function InputForFormattedText( @@ -64,10 +65,12 @@ function InputForFormattedText( ) : ( <> <HtmlFormatter> - <FormattedPreviewBox - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeUserInput(formattedValue ?? '') }} - /> + <SafeHTMLInjection + htmlAsString={formattedValue ?? ''} + sanitizeLevel={SanitizeLevel.USER_INPUT} + > + <FormattedPreviewBox /> + </SafeHTMLInjection> </HtmlFormatter> <ButtonSecondary className="sw-mt-2" onClick={props.onEditing} icon={<PencilIcon />}> diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx index 8666c9bdbc2..9beb4beb9af 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Action.tsx @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { Badge, Card, LinkBox, LinkIcon, SubHeading, Tabs } from 'design-system'; + +import { Badge, Card, LinkBox, LinkIcon, SafeHTMLInjection, SubHeading, Tabs } from 'design-system'; import * as React from 'react'; import { queryToSearchString } from '~sonar-aligned/helpers/urls'; import { translate, translateWithParameters } from '../../../helpers/l10n'; @@ -95,11 +96,9 @@ export default function Action(props: Props) { {action.deprecatedSince && <DeprecatedBadge since={action.deprecatedSince} />} </header> - <div - className="sw-mt-4 markdown" - // Safe: comes from the backend - dangerouslySetInnerHTML={{ __html: action.description }} - /> + <SafeHTMLInjection htmlAsString={action.description}> + <div className="sw-mt-4 markdown" /> + </SafeHTMLInjection> <div className="sw-mt-4"> <Tabs options={tabOptions} onChange={(opt) => setTab(opt)} value={tab} /> diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx index 711dc6c8c4a..76ed0840db7 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx @@ -17,7 +17,8 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { SubTitle } from 'design-system'; + +import { SafeHTMLInjection, SubTitle } from 'design-system'; import { isEmpty } from 'lodash'; import * as React from 'react'; import { WebApi } from '../../../types/types'; @@ -45,11 +46,9 @@ export default function Domain({ domain, query }: Props) { </header> {!isEmpty(domain.description) && ( - <div - className="sw-mt-3 markdown" - // Safe: comes from the backend - dangerouslySetInnerHTML={{ __html: domain.description }} - /> + <SafeHTMLInjection htmlAsString={domain.description}> + <div className="sw-mt-3 markdown" /> + </SafeHTMLInjection> )} <div className="sw-mt-4 sw-flex sw-flex-col sw-gap-4"> diff --git a/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx b/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx index 865fc766b09..b88f080e86e 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx +++ b/server/sonar-web/src/main/js/apps/web-api/components/Params.tsx @@ -17,7 +17,16 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { ContentCell, DarkLabel, HtmlFormatter, Note, Table, TableRow } from 'design-system'; + +import { + ContentCell, + DarkLabel, + HtmlFormatter, + Note, + SafeHTMLInjection, + Table, + TableRow, +} from 'design-system'; import * as React from 'react'; import { translate, translateWithParameters } from '../../../helpers/l10n'; import { WebApi } from '../../../types/types'; @@ -108,11 +117,9 @@ export default class Params extends React.PureComponent<Props> { {this.renderKey(param)} <ContentCell> - <div - className="markdown" - // Safe: comes from the backend - dangerouslySetInnerHTML={{ __html: param.description }} - /> + <SafeHTMLInjection htmlAsString={param.description}> + <div className="markdown" /> + </SafeHTMLInjection> </ContentCell> <ContentCell> diff --git a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx index ca1341902e4..a5cf16b7a96 100644 --- a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx +++ b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx @@ -22,6 +22,7 @@ import { CodeSyntaxHighlighter, FlagMessage, HtmlFormatter, + SanitizeLevel, ToggleButton, themeBorder, themeColor, @@ -30,7 +31,6 @@ import * as React from 'react'; import { RuleDescriptionSection, RuleDescriptionSections } from '../../apps/coding-rules/rule'; import applyCodeDifferences from '../../helpers/code-difference'; import { translate, translateWithParameters } from '../../helpers/l10n'; -import { sanitizeString } from '../../helpers/sanitize'; import { isDefined } from '../../helpers/types'; import { Cve as CveDetailsType } from '../../types/cves'; import { CveDetails } from './CveDetails'; @@ -147,8 +147,9 @@ export default class RuleDescription extends React.PureComponent<Props, State> { {isDefined(introductionSection) && ( <CodeSyntaxHighlighter className="rule-desc" - htmlAsString={sanitizeString(introductionSection)} + htmlAsString={introductionSection} language={language} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} /> )} {defaultContext && ( @@ -180,8 +181,9 @@ export default class RuleDescription extends React.PureComponent<Props, State> { <OtherContextOption /> ) : ( <CodeSyntaxHighlighter - htmlAsString={sanitizeString(selectedContext.content)} + htmlAsString={selectedContext.content} language={language} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} /> )} @@ -200,14 +202,16 @@ export default class RuleDescription extends React.PureComponent<Props, State> { {isDefined(introductionSection) && ( <CodeSyntaxHighlighter className="rule-desc" - htmlAsString={sanitizeString(introductionSection)} + htmlAsString={introductionSection} language={language} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} /> )} <CodeSyntaxHighlighter - htmlAsString={sanitizeString(sections[0].content)} + htmlAsString={sections[0].content} language={language} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} /> {cve && <CveDetails cve={cve} />} diff --git a/server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx b/server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx index fe90bf7bfa6..f65317b396d 100644 --- a/server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx +++ b/server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx @@ -18,6 +18,7 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import { render } from '@testing-library/react'; +import { SafeHTMLInjection } from 'design-system/lib'; import React from 'react'; import applyCodeDifferences from '../code-difference'; @@ -146,12 +147,8 @@ public void endpoint(HttpServletRequest request, HttpServletResponse response) t function renderDom(codeSnippet: string) { return render( - <div - className="markdown" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ - __html: codeSnippet, - }} - />, + <SafeHTMLInjection htmlAsString={codeSnippet}> + <div className="markdown" /> + </SafeHTMLInjection>, ); } diff --git a/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts b/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts deleted file mode 100644 index f219b154746..00000000000 --- a/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { sanitizeString, sanitizeStringRestricted } from '../sanitize'; - -describe('sanitizeStringRestricted', () => { - it('should preserve only specific formatting tags', () => { - expect( - sanitizeStringRestricted(` - Hi <a href="http://example.com" target="_blank">this</a> is <i>in italics</i> and <ul> - <li> lists </li> - <li> are allowed</li> - </ul> - <p> - as well. This is <b>Amazing</b> and this <strong>bold</strong> <br> - and <code>code.is.accepted too</code> - </p> - `), - ).toBe(` - Hi <a target="_blank" href="http://example.com">this</a> is <i>in italics</i> and <ul> - <li> lists </li> - <li> are allowed</li> - </ul> - <p> - as well. This is <b>Amazing</b> and this <strong>bold</strong> <br> - and <code>code.is.accepted too</code> - </p> - `); - }); - - /* - * Test code borrowed from OWASP's sanitizer tests - * https://github.com/OWASP/java-html-sanitizer/blob/master/src/test/resources/org/owasp/html/htmllexerinput1.html - */ - it('should strip everything else', () => { - const clean = sanitizeStringRestricted(`<?xml version="not-even-close"?> - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> - - <!-- a test input for HtmlLexer --> - - <html> - <head> - <title>Test File For HtmlLexer & HtmlParser</title> - <link rel=stylesheet type="text/css" src=foo/bar.css /> - <body - bgcolor=white - linkcolor = "blue" - onload="document.writeln( - "<p>properly escaped code in a handler</p>");" - > - - <script type="text/javascript"><!-- - document.writeln("<p>Some initialization code in global context</p>"); - --></script> - - <script type="text/javascript"> - // hi there - document.writeln("<p>More initialization</p>"); - </script> - - <div id=clickydiv onclick="handleClicky(event)" - ondblclick=this.onclick(event);return(false)> - Clicky - </div> - - <input id=foo> - <gxp:attr name="onchange">alert("<b>hi</b>");</gxp:attr> - </input> - - <pre><div id=notarealtag onclick=notcode()></pre> - - <!-- some tokenization corner cases --> - - < notatag <atag/> - - </ notatag> </redundantlyclosed/> - - <messyattributes a=b=c d="e"f=g h =i j= k l = m checked n="o"/> - - < < < all in one text block > > > - - <xmp>Make sure that <!-- comments don't obscure the xmp close</xmp> - <% # some php code here - write("<pre>$horriblySyntacticConstruct1</pre>\n\n"); - %> - <script type="text/javascript"><!-- - alert("hello world"); - // --></script> - - <script>/* </script> */alert('hi');</script> - <script><!--/* </script> */alert('hi');--></script> - - <xmp style=color:blue><!--/* </xmp> */alert('hi');--></xmp> - - <style><!-- p { contentf: '</style>' } --></style> - <style>Foo<!-- > </style> --></style> - <textarea><!-- Zoicks </textarea>--></textarea> - <!-- An escaping text span start may share its U+002D HYPHEN-MINUS characters - - with its corresponding escaping text span end. --> - <script><!--></script> - <script><!---></script> - <script><!----></script> - </body> - </html> - <![CDATA[ No such thing as a CDATA> section in HTML ]]> - <script>a<b</script> - <img src=foo.gif /><a href=><a href=/> - <span title=malformed attribs' do=don't id=foo checked onclick="a<b">Bar</span>`); - - expect(clean.replace(/\s+/g, '')).toBe( - `Clickyalert("<b>hi</b>");<divid=notarealtagonclick=notcode()><notatag<<<allinonetextblock>>><%#somephpcodeherewrite("$horriblySyntacticConstruct1");%>*/alert('hi');*/alert('hi');-->*/alert('hi');-->'}-->--><!--Zoicks-->sectioninHTML]]><ahref=""></a><ahref="/">Bar</a>`, - ); - }); -}); - -describe('sanitizeString', () => { - it('should not allow MathML and SVG', () => { - const tainted = ` - Hi <a href="javascript:alert('hello')" target="_blank">this</a> is <i>in italics</i> and <ul> - <li> lists </li> - <li> are allowed</li> - </ul> - <p class="some-class"> - as well. This is <b>Amazing</b> and this <strong>bold</strong> <br> - and <code>code.is.accepted too</code> - </p> - <svg><text>SVG isn't allowed</text></svg> - <math xmlns="http://www.w3.org/1998/Math/MathML"> - <infinity /> - </math>`; - const clean = ` - Hi <a>this</a> is <i>in italics</i> and <ul> - <li> lists </li> - <li> are allowed</li> - </ul> - <p class="some-class"> - as well. This is <b>Amazing</b> and this <strong>bold</strong> <br> - and <code>code.is.accepted too</code> - </p>`; - - expect(sanitizeString(tainted).trimRight()).toBe(clean); - }); -}); diff --git a/server/sonar-web/src/main/js/helpers/code-difference.ts b/server/sonar-web/src/main/js/helpers/code-difference.ts index ff7653546b2..2caaae10460 100644 --- a/server/sonar-web/src/main/js/helpers/code-difference.ts +++ b/server/sonar-web/src/main/js/helpers/code-difference.ts @@ -17,9 +17,10 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + +import { sanitizeHTMLNoSVGNoMathML } from 'design-system'; import { diffLines } from 'diff'; import { groupBy, keyBy } from 'lodash'; -import { sanitizeString } from './sanitize'; const NUMBER_OF_EXAMPLES = 2; @@ -85,7 +86,7 @@ function differentiateCode(compliant: string, nonCompliant: string) { nonCompliantCode += `<div class='code-removed'>${value}</div>`; } }); - return [sanitizeString(nonCompliantCode), sanitizeString(compliantCode)]; + return [sanitizeHTMLNoSVGNoMathML(nonCompliantCode), sanitizeHTMLNoSVGNoMathML(compliantCode)]; } function replaceInDom(current: Element, code: string) { diff --git a/server/sonar-web/src/main/js/helpers/sanitize.ts b/server/sonar-web/src/main/js/helpers/sanitize.ts deleted file mode 100644 index acdae5799ca..00000000000 --- a/server/sonar-web/src/main/js/helpers/sanitize.ts +++ /dev/null @@ -1,57 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2024 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -import { sanitize } from 'dompurify'; - -export function sanitizeStringRestricted(html: string) { - return sanitize(html, { - ALLOWED_TAGS: ['b', 'br', 'code', 'i', 'li', 'p', 'strong', 'ul', 'a'], - ALLOWED_ATTR: ['target', 'href'], - }); -} - -export function sanitizeString(html: string) { - return sanitize(html, { USE_PROFILES: { html: true } }); -} - -export function sanitizeUserInput(html: string) { - return sanitize(html, { - ALLOWED_TAGS: [ - 'b', - 'br', - 'code', - 'i', - 'li', - 'p', - 'strong', - 'ul', - 'ol', - 'a', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'blockquote', - 'pre', - ], - ALLOWED_ATTR: ['target', 'href', 'rel'], - }); -} diff --git a/server/sonar-web/yarn.lock b/server/sonar-web/yarn.lock index b8a763a5a3d..62f81b59ef4 100644 --- a/server/sonar-web/yarn.lock +++ b/server/sonar-web/yarn.lock @@ -8723,6 +8723,7 @@ __metadata: d3-shape: 3.2.0 d3-zoom: 3.0.0 date-fns: 4.1.0 + dompurify: 3.1.7 lodash: 4.17.21 react: 18.3.1 react-day-picker: 8.10.0 |