From: David Cho-Lerat Date: Thu, 3 Oct 2024 08:09:00 +0000 (+0200) Subject: SONAR-23249 Fix SSF-656 & SSF-657 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=c9ecf7fa2f9af18f474a2aea2c7c3cefd3a6e80f;p=sonarqube.git SONAR-23249 Fix SSF-656 & SSF-657 --- 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) { + 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 ( - + + + ); } 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 ? ( - + + + ) : ( {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 ( {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 = ` + + + + + + + +Test File For HtmlLexer & HtmlParser + + + + + + + +
+Clicky +
+ + +alert("<b>hi</b>"); + + +
<div id=notarealtag onclick=notcode()>
+ + + +< notatag + + + + + +< < < all in one text block > > > + +Make sure that <!-- comments don't obscure the xmp close +<% # some php code here +write("
$horriblySyntacticConstruct1
\n\n"); +%> + + + */alert('hi'); + */alert('hi');--> + +<!--/* */alert('hi');--> + +' } --> + --> +--> + + + + + +This is bold and this is italic and this is underlined. +
+A
quote
and a code and a
pre
. +An

h1

and an

h2

and an

h3

and an

h4

and an
h5
and an
h6
. +An
  1. ol
and a
  • ul
and a

p

. +A strong and a link + +this is wrong + +SVG isn't always allowed + + + + + + + + section in HTML ]]> + + +Bar`; + +describe('sanitizeHTMLToPreventCSSInjection', () => { + it('should strip off style 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');--> ' } --> + --> + --> + This is bold and this is italic and this is underlined. +
+ A
quote
and a code and a
pre
. + An

h1

and an

h2

and an

h3

and an

h4

and an
h5
and an
h6
. + An
  1. ol
and a
  • ul
and a

p

. + A strong and a
link + this is wrong + SVG isn't always allowed + + + section in HTML ]]> + + Bar`; + + expect(sanitizeHTMLToPreventCSSInjection(tainted).trimEnd().replace(/\s+/g, ' ')).toBe( + clean.replace(/\s+/g, ' '), + ); + }); +}); + +describe('sanitizeHTMLNoSVGNoMathML', () => { + it('should not allow MathML and SVG', () => { + 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');--> ' } --> + --> + --> + This is bold and this is italic and this is underlined. +
+ A
quote
and a code and a
pre
. + An

h1

and an

h2

and an

h3

and an

h4

and an
h5
and an
h6
. + An
  1. ol
and a
  • ul
and a

p

. + A strong and a link + this is wrong + section in HTML ]]> + + Bar`; + + 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>"); +
<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 bold and this is italic and this is underlined. +
+ A
quote
and a code and a
pre
. + An

h1

and an

h2

and an

h3

and an

h4

and an
h5
and an
h6
. + An
  1. ol
and a
  • ul
and a

p

. + A strong and a link + this is wrong + section in HTML ]]> + + Bar`; + + 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 bold and this is italic and this is underlined. +
+ A quote and a code and a pre. + An h1 and an h2 and an h3 and an h4 and an h5 and an h6. + An
  • ol
  • and a
    • ul
    and a

    p

    . + A strong and a link + this is wrong + section in HTML ]]> + + Bar`; + + 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 = ` + + + + + + +

    a stylish paragraph

    + + SVG isn't always allowed + + + + + + `; + + render(); + + 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: + * ``` + * + * ``` + * + * @example + * Here's an example with a custom `sanitizeLevel` and a child `div`: + * ``` + * + * // the HTML will be safely injected in the div below, with the className preserved: + *
    + * + * ``` + */ +export const SafeHTMLInjection = ({ + children, + htmlAsString, + sanitizeLevel = SanitizeLevel.FORBID_STYLE, +}: Readonly<{ + children?: React.ReactElement; + htmlAsString: string; + sanitizeLevel?: SanitizeLevel; +}>) => + React.cloneElement(children ?? , { + 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 {
    - ')), - }} + ')} + sanitizeLevel={SanitizeLevel.RESTRICTED} /> 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) { /> )} {param.htmlDesc !== undefined && ( - + + + )} )) 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) { value={actualValue} /> )} + {param.htmlDesc !== undefined && ( - + + + )} ); 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) { {ruleDetails.htmlNote !== undefined && ( )} 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) {
    {param.htmlDesc !== undefined && ( -
    + +
    + )} {param.defaultValue !== undefined && ( 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) { const { issue } = props; const [changeLog, setChangeLog] = React.useState([]); 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) {
    + {user.name && (
    @@ -112,11 +117,9 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) { {type === ReviewHistoryType.Comment && key && html && markdown && (
    - + + + {updatable && (
    @@ -127,6 +130,7 @@ export default function IssueReviewHistory(props: HotspotReviewHistoryProps) { size="small" stopPropagation={false} /> + - ')), - }} + ')} + sanitizeLevel={SanitizeLevel.RESTRICTED} /> 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) { 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 && (
    - + + + {updatable && (
    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) { )} {message !== undefined && message.length > 0 && ( - + + + )} {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) { {description && ( -
    + +
    + )} 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 )} + {subCategory.description != null && ( - + + + )} + - + + + }> 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 && } -
    + +
    +
    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) { {!isEmpty(domain.description) && ( -
    + +
    + )}
    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 { {this.renderKey(param)} -
    + +
    + 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 { {isDefined(introductionSection) && ( )} {defaultContext && ( @@ -180,8 +181,9 @@ export default class RuleDescription extends React.PureComponent { ) : ( )} @@ -200,14 +202,16 @@ export default class RuleDescription extends React.PureComponent { {isDefined(introductionSection) && ( )} {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( -
    , + +
    + , ); } 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 this is in italics and
      -
    • lists
    • -
    • are allowed
    • -
    -

    - as well. This is Amazing and this bold
    - and code.is.accepted too -

    - `), - ).toBe(` - Hi this is in italics and
      -
    • lists
    • -
    • are allowed
    • -
    -

    - as well. This is Amazing and this bold
    - and code.is.accepted too -

    - `); - }); - - /* - * 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(` - - - - - - - - Test File For HtmlLexer & HtmlParser - - - - - - - -
    - Clicky -
    - - - alert("<b>hi</b>"); - - -
    <div id=notarealtag onclick=notcode()>
    - - - - < notatag - - - - - - < < < all in one text block > > > - - Make sure that <!-- comments don't obscure the xmp close - <% # some php code here - write("
    $horriblySyntacticConstruct1
    \n\n"); - %> - - - */alert('hi'); - */alert('hi');--> - - <!--/* */alert('hi');--> - - ' } --> - --> - --> - - - - - - - section in HTML ]]> - - - Bar`); - - 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]]>Bar`, - ); - }); -}); - -describe('sanitizeString', () => { - it('should not allow MathML and SVG', () => { - const tainted = ` - Hi this is in italics and
      -
    • lists
    • -
    • are allowed
    • -
    -

    - as well. This is Amazing and this bold
    - and code.is.accepted too -

    - SVG isn't allowed - - - `; - const clean = ` - Hi this is in italics and
      -
    • lists
    • -
    • are allowed
    • -
    -

    - as well. This is Amazing and this bold
    - and code.is.accepted too -

    `; - - 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 += `
    ${value}
    `; } }); - 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