diff options
Diffstat (limited to 'server/sonar-web')
37 files changed, 930 insertions, 715 deletions
diff --git a/server/sonar-web/.eslintrc b/server/sonar-web/.eslintrc index ca97fff5e6d..f9fce6b0894 100644 --- a/server/sonar-web/.eslintrc +++ b/server/sonar-web/.eslintrc @@ -3,7 +3,29 @@ "rules": { "camelcase": "off", "promise/no-return-wrap": "warn", + "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-curly-brace-presence": "warn", "testing-library/render-result-naming-convention": "off" } -} +}
\ No newline at end of file diff --git a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts index 1f58dcb7458..7ac527b9b18 100644 --- a/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts +++ b/server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.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. */ + import { FormattedMessage } from 'react-intl'; import NotFound from '../../../app/components/NotFound'; import A11ySkipTarget from '../../../components/a11y/A11ySkipTarget'; @@ -103,7 +104,7 @@ import { postJSONBody, request, } from '../../../helpers/request'; -import { sanitizeStringRestricted } from '../../../helpers/sanitize'; +import { sanitizeHTMLRestricted } from '../../../helpers/sanitize'; import { getStandards, renderCWECategory, @@ -166,7 +167,7 @@ const exposeLibraries = () => { getComponentSecurityHotspotsUrl, getMeasureHistoryUrl, getRulesUrl, - sanitizeStringRestricted, + sanitizeStringRestricted: sanitizeHTMLRestricted, }; }, }); diff --git a/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx b/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx index 1a7b7f91856..0035fe6e482 100644 --- a/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx +++ b/server/sonar-web/src/main/js/app/components/search/SearchResult.tsx @@ -17,11 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; import Link from '../../../components/common/Link'; import ClockIcon from '../../../components/icons/ClockIcon'; import FavoriteIcon from '../../../components/icons/FavoriteIcon'; import QualifierIcon from '../../../components/icons/QualifierIcon'; +import { SafeHTMLInjection } from '../../../helpers/sanitize'; import { getComponentOverviewUrl } from '../../../helpers/urls'; import { ComponentResult } from './utils'; @@ -60,12 +62,9 @@ export default class SearchResult extends React.PureComponent<Props> { </span> {component.match ? ( - <span - className="navbar-search-item-match" - // Safe: comes from the search engine, that injects bold tags into component names - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: component.match }} - /> + <SafeHTMLInjection htmlAsString={component.match}> + <span className="navbar-search-item-match" /> + </SafeHTMLInjection> ) : ( <span className="navbar-search-item-match">{component.name}</span> )} diff --git a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap index 18f2646901e..df492ffecc1 100644 --- a/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap +++ b/server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap @@ -80,14 +80,13 @@ exports[`renders match 1`] = ` qualifier="TRK" /> </span> - <span - className="navbar-search-item-match" - dangerouslySetInnerHTML={ - { - "__html": "f<mark>o</mark>o", - } - } - /> + <SafeHTMLInjection + htmlAsString="f<mark>o</mark>o" + > + <span + className="navbar-search-item-match" + /> + </SafeHTMLInjection> </div> <div className="navbar-search-item-right text-muted-2" 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 70851441576..531c0162257 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 @@ -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 classNames from 'classnames'; import * as React from 'react'; import { OptionTypeBase } from 'react-select'; @@ -26,7 +27,7 @@ import Modal from '../../../components/controls/Modal'; import Select from '../../../components/controls/Select'; import { Alert } from '../../../components/ui/Alert'; import { translate } from '../../../helpers/l10n'; -import { sanitizeString } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { Dict, Rule, RuleActivation, RuleDetails } from '../../../types/types'; import { sortProfiles } from '../../quality-profiles/utils'; import { SeveritySelect } from './SeveritySelect'; @@ -218,11 +219,12 @@ export default class ActivationFormModal extends React.PureComponent<Props, Stat /> )} {param.htmlDesc !== undefined && ( - <div - className="note" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }} - /> + <SafeHTMLInjection + htmlAsString={param.htmlDesc} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <div className="note" /> + </SafeHTMLInjection> )} </div> )) 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 57c8f242761..55d67cebfad 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 * as React from 'react'; import { components, OptionProps, OptionTypeBase, SingleValueProps } from 'react-select'; import { createRule, updateRule } from '../../../api/rules'; @@ -31,7 +32,7 @@ import MandatoryFieldsExplanation from '../../../components/ui/MandatoryFieldsEx import { RULE_STATUSES, RULE_TYPES } from '../../../helpers/constants'; import { csvEscape } from '../../../helpers/csv'; import { translate } from '../../../helpers/l10n'; -import { sanitizeString } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { latinize } from '../../../helpers/strings'; import { Dict, RuleDetails, RuleParameter } from '../../../types/types'; import { SeveritySelect } from './SeveritySelect'; @@ -317,11 +318,12 @@ export default class CustomRuleFormModal extends React.PureComponent<Props, Stat /> )} {param.htmlDesc !== undefined && ( - <div - className="modal-field-description" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }} - /> + <SafeHTMLInjection + htmlAsString={param.htmlDesc} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <div className="modal-field-description" /> + </SafeHTMLInjection> )} </div> ); 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 a7fd2cb2c89..8fe97276bc6 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 @@ -17,13 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; import { updateRule } from '../../../api/rules'; import FormattingTips from '../../../components/common/FormattingTips'; import { Button, ResetButtonLink } from '../../../components/controls/buttons'; import RuleTabViewer from '../../../components/rules/RuleTabViewer'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { sanitizeString, sanitizeUserInput } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { RuleDetails } from '../../../types/types'; import { RuleDescriptionSections } from '../rule'; import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal'; @@ -112,14 +113,14 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S renderExtendedDescription = () => ( <div id="coding-rules-detail-description-extra"> {this.props.ruleDetails.htmlNote !== undefined && ( - <div - className="rule-desc spacer-bottom markdown" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ - __html: sanitizeUserInput(this.props.ruleDetails.htmlNote), - }} - /> + <SafeHTMLInjection + htmlAsString={this.props.ruleDetails.htmlNote} + sanitizeLevel={SanitizeLevel.USER_INPUT} + > + <div className="rule-desc spacer-bottom markdown" /> + </SafeHTMLInjection> )} + {this.props.canWrite && ( <Button id="coding-rules-detail-extend-description" @@ -216,23 +217,28 @@ export default class RuleDetailsDescription extends React.PureComponent<Props, S return ( <div className="js-rule-description"> {defaultSection && ( - <section - className="coding-rules-detail-description markdown" - key={defaultSection.key} - /* eslint-disable-next-line react/no-danger */ - dangerouslySetInnerHTML={{ __html: sanitizeString(defaultSection.content) }} - /> + <SafeHTMLInjection + htmlAsString={defaultSection.content} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <section + className="coding-rules-detail-description markdown" + key={defaultSection.key} + /> + </SafeHTMLInjection> )} {hasDescriptionSection && !defaultSection && ( <> {introductionSection && ( - <div - className="rule-desc" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeString(introductionSection) }} - /> + <SafeHTMLInjection + htmlAsString={introductionSection} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <div className="rule-desc" /> + </SafeHTMLInjection> )} + <RuleTabViewer ruleDetails={ruleDetails} /> </> )} 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 512219441a3..50a50c4adb5 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,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 * as React from 'react'; import { translate } from '../../../helpers/l10n'; -import { sanitizeString } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { RuleParameter } from '../../../types/types'; interface Props { @@ -30,13 +31,17 @@ export default class RuleDetailsParameters extends React.PureComponent<Props> { renderParameter = (param: RuleParameter) => ( <tr className="coding-rules-detail-parameter" key={param.key}> <td className="coding-rules-detail-parameter-name">{param.key}</td> + <td className="coding-rules-detail-parameter-description"> {param.htmlDesc !== undefined && ( - <p - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeString(param.htmlDesc) }} - /> + <SafeHTMLInjection + htmlAsString={param.htmlDesc} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <p /> + </SafeHTMLInjection> )} + {param.defaultValue !== undefined && ( <div className="note spacer-top"> {translate('coding_rules.parameters.default_value')} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap index 218e1a3d16e..680fb4d5d02 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap @@ -160,14 +160,14 @@ exports[`should render correctly: default 1`] = ` type="text" value="1" /> - <div - className="note" - dangerouslySetInnerHTML={ - { - "__html": "description", - } - } - /> + <SafeHTMLInjection + htmlAsString="description" + sanitizeLevel={1} + > + <div + className="note" + /> + </SafeHTMLInjection> </div> <div className="modal-field" @@ -281,14 +281,14 @@ exports[`should render correctly: submitting 1`] = ` type="text" value="1" /> - <div - className="note" - dangerouslySetInnerHTML={ - { - "__html": "description", - } - } - /> + <SafeHTMLInjection + htmlAsString="description" + sanitizeLevel={1} + > + <div + className="note" + /> + </SafeHTMLInjection> </div> <div className="modal-field" @@ -400,14 +400,14 @@ exports[`should render correctly: update mode 1`] = ` type="text" value="1" /> - <div - className="note" - dangerouslySetInnerHTML={ - { - "__html": "description", - } - } - /> + <SafeHTMLInjection + htmlAsString="description" + sanitizeLevel={1} + > + <div + className="note" + /> + </SafeHTMLInjection> </div> <div className="modal-field" @@ -555,14 +555,14 @@ exports[`should render correctly: with deep profiles 1`] = ` type="text" value="1" /> - <div - className="note" - dangerouslySetInnerHTML={ - { - "__html": "description", - } - } - /> + <SafeHTMLInjection + htmlAsString="description" + sanitizeLevel={1} + > + <div + className="note" + /> + </SafeHTMLInjection> </div> <div className="modal-field" diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap index 77ad50e79a5..c376249c83a 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap @@ -210,14 +210,14 @@ exports[`should handle re-activation 1`] = ` type="text" value="" /> - <div - className="modal-field-description" - dangerouslySetInnerHTML={ - { - "__html": "description", - } - } - /> + <SafeHTMLInjection + htmlAsString="description" + sanitizeLevel={1} + > + <div + className="modal-field-description" + /> + </SafeHTMLInjection> </div> <div className="modal-field" @@ -465,14 +465,14 @@ exports[`should render correctly: default 1`] = ` type="text" value="" /> - <div - className="modal-field-description" - dangerouslySetInnerHTML={ - { - "__html": "description", - } - } - /> + <SafeHTMLInjection + htmlAsString="description" + sanitizeLevel={1} + > + <div + className="modal-field-description" + /> + </SafeHTMLInjection> </div> <div className="modal-field" diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsParameters-test.tsx.snap b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsParameters-test.tsx.snap index 1c441e4738e..b6f155022ac 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsParameters-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsParameters-test.tsx.snap @@ -25,13 +25,12 @@ exports[`should render correctly 1`] = ` <td className="coding-rules-detail-parameter-description" > - <p - dangerouslySetInnerHTML={ - { - "__html": "description", - } - } - /> + <SafeHTMLInjection + htmlAsString="description" + sanitizeLevel={1} + > + <p /> + </SafeHTMLInjection> <div className="note spacer-top" > @@ -57,13 +56,12 @@ exports[`should render correctly 1`] = ` <td className="coding-rules-detail-parameter-description" > - <p - dangerouslySetInnerHTML={ - { - "__html": "description", - } - } - /> + <SafeHTMLInjection + htmlAsString="description" + sanitizeLevel={1} + > + <p /> + </SafeHTMLInjection> <div className="note spacer-top" > 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 f0dfe918a19..d08a688e1d2 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 classNames from 'classnames'; import * as React from 'react'; import { Button, ButtonLink, DeleteButton, EditButton } from '../../../components/controls/buttons'; @@ -27,7 +28,7 @@ import IssueChangelogDiff from '../../../components/issue/components/IssueChange import Avatar from '../../../components/ui/Avatar'; import { PopupPlacement } from '../../../components/ui/popups'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { Hotspot, ReviewHistoryType } from '../../../types/security-hotspots'; import { getHotspotReviewHistory } from '../utils'; import HotspotCommentPopup from './HotspotCommentPopup'; @@ -103,11 +104,10 @@ export default function HotspotReviewHistory(props: HotspotReviewHistoryProps) { {type === ReviewHistoryType.Comment && key && html && markdown && ( <div className="spacer-top display-flex-space-between"> - <div - className="markdown" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeUserInput(html) }} - /> + <SafeHTMLInjection htmlAsString={html} sanitizeLevel={SanitizeLevel.USER_INPUT}> + <div className="markdown" /> + </SafeHTMLInjection> + {updatable && ( <div> <div className="dropdown"> diff --git a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap index 98e92766989..01b25206020 100644 --- a/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap @@ -113,14 +113,14 @@ exports[`should render correctly: default 1`] = ` <div className="spacer-top display-flex-space-between" > - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "<strong>TEST</strong>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<strong>TEST</strong>" + sanitizeLevel={2} + > + <div + className="markdown" + /> + </SafeHTMLInjection> </div> </li> <li @@ -155,14 +155,14 @@ exports[`should render correctly: default 1`] = ` <div className="spacer-top display-flex-space-between" > - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "<strong>TEST</strong>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<strong>TEST</strong>" + sanitizeLevel={2} + > + <div + className="markdown" + /> + </SafeHTMLInjection> </div> </li> <li @@ -197,14 +197,14 @@ exports[`should render correctly: default 1`] = ` <div className="spacer-top display-flex-space-between" > - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "<strong>TEST</strong>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<strong>TEST</strong>" + sanitizeLevel={2} + > + <div + className="markdown" + /> + </SafeHTMLInjection> </div> </li> </ul> @@ -358,14 +358,14 @@ exports[`should render correctly: show full list 1`] = ` <div className="spacer-top display-flex-space-between" > - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "<strong>TEST</strong>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<strong>TEST</strong>" + sanitizeLevel={2} + > + <div + className="markdown" + /> + </SafeHTMLInjection> </div> </li> <li @@ -400,14 +400,14 @@ exports[`should render correctly: show full list 1`] = ` <div className="spacer-top display-flex-space-between" > - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "<strong>TEST</strong>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<strong>TEST</strong>" + sanitizeLevel={2} + > + <div + className="markdown" + /> + </SafeHTMLInjection> </div> </li> <li @@ -442,14 +442,14 @@ exports[`should render correctly: show full list 1`] = ` <div className="spacer-top display-flex-space-between" > - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "<strong>TEST</strong>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<strong>TEST</strong>" + sanitizeLevel={2} + > + <div + className="markdown" + /> + </SafeHTMLInjection> </div> </li> <li @@ -484,14 +484,14 @@ exports[`should render correctly: show full list 1`] = ` <div className="spacer-top display-flex-space-between" > - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "<strong>TEST</strong>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<strong>TEST</strong>" + sanitizeLevel={2} + > + <div + className="markdown" + /> + </SafeHTMLInjection> </div> </li> <li @@ -526,14 +526,14 @@ exports[`should render correctly: show full list 1`] = ` <div className="spacer-top display-flex-space-between" > - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "<strong>TEST</strong>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<strong>TEST</strong>" + sanitizeLevel={2} + > + <div + className="markdown" + /> + </SafeHTMLInjection> <div> <div className="dropdown" 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 e6943d126bf..d7b134d53db 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 @@ -17,12 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; import { Location } from '../../../components/hoc/withRouter'; import { Alert } from '../../../components/ui/Alert'; import DeferredSpinner from '../../../components/ui/DeferredSpinner'; import { translate } from '../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { getReturnUrl } from '../../../helpers/urls'; import { IdentityProvider } from '../../../types/types'; import './Login.css'; @@ -59,11 +60,9 @@ export default function Login(props: LoginProps) { )} {message && ( - <div - className="login-message markdown big-padded spacer-top huge-spacer-bottom" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeUserInput(message) }} - /> + <SafeHTMLInjection htmlAsString={message} sanitizeLevel={SanitizeLevel.USER_INPUT}> + <div className="login-message markdown big-padded spacer-top huge-spacer-bottom" /> + </SafeHTMLInjection> )} {identityProviders.length > 0 && ( diff --git a/server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx b/server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx index 13163d279ca..3a8a0c43eac 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx @@ -17,13 +17,14 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import classNames from 'classnames'; import * as React from 'react'; import Tooltip from '../../../components/controls/Tooltip'; import AlertErrorIcon from '../../../components/icons/AlertErrorIcon'; import AlertSuccessIcon from '../../../components/icons/AlertSuccessIcon'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { sanitizeStringRestricted } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { ExtendedSettingDefinition, SettingValue } from '../../../types/settings'; import { combineDefinitionAndSettingValue, @@ -52,7 +53,7 @@ export interface DefinitionRendererProps { const formNoop = (e: React.FormEvent<HTMLFormElement>) => e.preventDefault(); -export default function DefinitionRenderer(props: DefinitionRendererProps) { +export default function DefinitionRenderer(props: Readonly<DefinitionRendererProps>) { const { changedValue, loading, validationMessage, settingValue, success, definition, isEditing } = props; @@ -78,11 +79,9 @@ export default function DefinitionRenderer(props: DefinitionRendererProps) { </h3> {description && ( - <div - className="markdown small spacer-top" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeStringRestricted(description) }} - /> + <SafeHTMLInjection htmlAsString={description} sanitizeLevel={SanitizeLevel.RESTRICTED}> + <div className="markdown small spacer-top" /> + </SafeHTMLInjection> )} <Tooltip overlay={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 3267c22cdb9..65664b469ae 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,10 +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 { groupBy, sortBy } from 'lodash'; import * as React from 'react'; import { Location, withRouter } from '../../../components/hoc/withRouter'; -import { sanitizeStringRestricted } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { SettingDefinitionAndValue } from '../../../types/settings'; import { Component } from '../../../types/types'; import { getSubCategoryDescription, getSubCategoryName } from '../utils'; @@ -91,15 +92,16 @@ export class SubCategoryDefinitionsList extends React.PureComponent<SubCategoryD {subCategory.name} </h2> )} + {subCategory.description != null && ( - <div - className="settings-sub-category-description markdown" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ - __html: sanitizeStringRestricted(subCategory.description), - }} - /> + <SafeHTMLInjection + htmlAsString={subCategory.description} + sanitizeLevel={SanitizeLevel.RESTRICTED} + > + <div className="settings-sub-category-description markdown" /> + </SafeHTMLInjection> )} + <DefinitionsList component={component} scrollToDefinition={this.scrollToSubCategoryOrDefinition} diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionRenderer-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionRenderer-test.tsx.snap index 021e807d1af..31031f47af8 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionRenderer-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionRenderer-test.tsx.snap @@ -14,14 +14,14 @@ exports[`should render correctly: changed value 1`] = ` > property.foo.name </h3> - <div - className="markdown small spacer-top" - dangerouslySetInnerHTML={ - { - "__html": "property.foo.description", - } - } - /> + <SafeHTMLInjection + htmlAsString="property.foo.description" + sanitizeLevel={3} + > + <div + className="markdown small spacer-top" + /> + </SafeHTMLInjection> <Tooltip overlay="settings.key_x.foo" > @@ -105,14 +105,14 @@ exports[`should render correctly: in error 1`] = ` > property.foo.name </h3> - <div - className="markdown small spacer-top" - dangerouslySetInnerHTML={ - { - "__html": "property.foo.description", - } - } - /> + <SafeHTMLInjection + htmlAsString="property.foo.description" + sanitizeLevel={3} + > + <div + className="markdown small spacer-top" + /> + </SafeHTMLInjection> <Tooltip overlay="settings.key_x.foo" > @@ -205,14 +205,14 @@ exports[`should render correctly: loading 1`] = ` > property.foo.name </h3> - <div - className="markdown small spacer-top" - dangerouslySetInnerHTML={ - { - "__html": "property.foo.description", - } - } - /> + <SafeHTMLInjection + htmlAsString="property.foo.description" + sanitizeLevel={3} + > + <div + className="markdown small spacer-top" + /> + </SafeHTMLInjection> <Tooltip overlay="settings.key_x.foo" > @@ -303,14 +303,14 @@ exports[`should render correctly: original value 1`] = ` > property.foo.name </h3> - <div - className="markdown small spacer-top" - dangerouslySetInnerHTML={ - { - "__html": "property.foo.description", - } - } - /> + <SafeHTMLInjection + htmlAsString="property.foo.description" + sanitizeLevel={3} + > + <div + className="markdown small spacer-top" + /> + </SafeHTMLInjection> <Tooltip overlay="settings.key_x.foo" > @@ -395,14 +395,14 @@ exports[`should render correctly: success 1`] = ` > property.foo.name </h3> - <div - className="markdown small spacer-top" - dangerouslySetInnerHTML={ - { - "__html": "property.foo.description", - } - } - /> + <SafeHTMLInjection + htmlAsString="property.foo.description" + sanitizeLevel={3} + > + <div + className="markdown small spacer-top" + /> + </SafeHTMLInjection> <Tooltip overlay="settings.key_x.foo" > @@ -493,14 +493,14 @@ exports[`should render correctly: with description 1`] = ` > property.foo.name </h3> - <div - className="markdown small spacer-top" - dangerouslySetInnerHTML={ - { - "__html": "property.foo.description", - } - } - /> + <SafeHTMLInjection + htmlAsString="property.foo.description" + sanitizeLevel={3} + > + <div + className="markdown small spacer-top" + /> + </SafeHTMLInjection> <Tooltip overlay="settings.key_x.foo" > diff --git a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap index 9e70241ec02..2e7ab454375 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap @@ -13,14 +13,14 @@ exports[`should render correctly 1`] = ` > property.category.general.email </h2> - <div - className="settings-sub-category-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "property.category.general.email.description", - } - } - /> + <SafeHTMLInjection + htmlAsString="property.category.general.email.description" + sanitizeLevel={3} + > + <div + className="settings-sub-category-description markdown" + /> + </SafeHTMLInjection> <DefinitionsList scrollToDefinition={[Function]} settings={ @@ -55,14 +55,14 @@ exports[`should render correctly 1`] = ` > property.category.general.qg </h2> - <div - className="settings-sub-category-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "property.category.general.qg.description", - } - } - /> + <SafeHTMLInjection + htmlAsString="property.category.general.qg.description" + sanitizeLevel={3} + > + <div + className="settings-sub-category-description markdown" + /> + </SafeHTMLInjection> <DefinitionsList scrollToDefinition={[Function]} settings={ @@ -101,14 +101,14 @@ exports[`should render correctly: subcategory 1`] = ` > property.category.general.qg </h2> - <div - className="settings-sub-category-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "property.category.general.qg.description", - } - } - /> + <SafeHTMLInjection + htmlAsString="property.category.general.qg.description" + sanitizeLevel={3} + > + <div + className="settings-sub-category-description markdown" + /> + </SafeHTMLInjection> <DefinitionsList scrollToDefinition={[Function]} settings={ 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 56ed64d587a..699a9fc56da 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 @@ -17,12 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; import FormattingTipsWithLink from '../../../../components/common/FormattingTipsWithLink'; import { Button } from '../../../../components/controls/buttons'; import EditIcon from '../../../../components/icons/EditIcon'; import { translate } from '../../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../../helpers/sanitize'; import { DefaultSpecializedInputProps } from '../../utils'; export default function InputForFormattedText(props: DefaultSpecializedInputProps) { @@ -51,11 +52,13 @@ export default function InputForFormattedText(props: DefaultSpecializedInputProp </div> ) : ( <> - <div - className="markdown-preview markdown" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeUserInput(formattedValue ?? '') }} - /> + <SafeHTMLInjection + htmlAsString={formattedValue ?? ''} + sanitizeLevel={SanitizeLevel.USER_INPUT} + > + <div className="markdown-preview markdown" /> + </SafeHTMLInjection> + <Button className="spacer-top" onClick={props.onEditing}> <EditIcon className="spacer-right" /> {translate('edit')} 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 3d0ce681a53..691e2118a82 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,11 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import classNames from 'classnames'; import * as React from 'react'; import Link from '../../../components/common/Link'; import LinkIcon from '../../../components/icons/LinkIcon'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { SafeHTMLInjection } from '../../../helpers/sanitize'; import { queryToSearch } from '../../../helpers/urls'; import { WebApi } from '../../../types/types'; import { getActionKey, serializeQuery } from '../utils'; @@ -177,11 +179,9 @@ export default class Action extends React.PureComponent<Props, State> { </header> <div className="boxed-group-inner"> - <div - className="web-api-action-description markdown" - // Safe: comes from the backend - dangerouslySetInnerHTML={{ __html: action.description }} - /> + <SafeHTMLInjection htmlAsString={action.description}> + <div className="web-api-action-description markdown" /> + </SafeHTMLInjection> {this.renderTabs()} 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 aa072b597ce..d7fed0b0053 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,9 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; +import { SafeHTMLInjection } from '../../../helpers/sanitize'; import { WebApi } from '../../../types/types'; import { actionsFilter, getActionKey, Query } from '../utils'; import Action from './Action'; @@ -51,11 +53,9 @@ export default function Domain({ domain, query }: Props) { </header> {domain.description && ( - <div - className="web-api-domain-description markdown" - // Safe: comes from the backend - dangerouslySetInnerHTML={{ __html: domain.description }} - /> + <SafeHTMLInjection htmlAsString={domain.description}> + <div className="web-api-domain-description markdown" /> + </SafeHTMLInjection> )} <div className="web-api-domain-actions"> 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 71af96386ce..da31aa4a883 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,8 +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 * as React from 'react'; import { translate, translateWithParameters } from '../../../helpers/l10n'; +import { SafeHTMLInjection } from '../../../helpers/sanitize'; import { WebApi } from '../../../types/types'; import DeprecatedBadge from './DeprecatedBadge'; import InternalBadge from './InternalBadge'; @@ -98,11 +100,9 @@ export default class Params extends React.PureComponent<Props> { {this.renderKey(param)} <td> - <div - className="markdown" - // Safe: comes from the backend - dangerouslySetInnerHTML={{ __html: param.description }} - /> + <SafeHTMLInjection htmlAsString={param.description}> + <div className="markdown" /> + </SafeHTMLInjection> </td> <td style={{ width: 250 }}> diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap index 6a646251d00..0e9b4f2e5f9 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap @@ -119,14 +119,13 @@ exports[`should render correctly 1`] = ` <div className="boxed-group-inner" > - <div - className="web-api-action-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "Foo Desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="Foo Desc" + > + <div + className="web-api-action-description markdown" + /> + </SafeHTMLInjection> <ul className="web-api-action-actions tabs" > diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Domain-test.tsx.snap b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Domain-test.tsx.snap index 36349a995c9..38aa9c97656 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Domain-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Domain-test.tsx.snap @@ -13,14 +13,13 @@ exports[`should also render actions with a description matching the query 1`] = api </h2> </header> - <div - className="web-api-domain-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "API Desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="API Desc" + > + <div + className="web-api-domain-description markdown" + /> + </SafeHTMLInjection> <div className="web-api-domain-actions" > @@ -139,14 +138,13 @@ exports[`should not render deprecated actions 1`] = ` api </h2> </header> - <div - className="web-api-domain-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "API Desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="API Desc" + > + <div + className="web-api-domain-description markdown" + /> + </SafeHTMLInjection> <div className="web-api-domain-actions" /> @@ -166,14 +164,13 @@ exports[`should not render internal actions 1`] = ` api </h2> </header> - <div - className="web-api-domain-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "API Desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="API Desc" + > + <div + className="web-api-domain-description markdown" + /> + </SafeHTMLInjection> <div className="web-api-domain-actions" /> @@ -193,14 +190,13 @@ exports[`should render deprecated actions 1`] = ` api </h2> </header> - <div - className="web-api-domain-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "API Desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="API Desc" + > + <div + className="web-api-domain-description markdown" + /> + </SafeHTMLInjection> <div className="web-api-domain-actions" > @@ -256,14 +252,13 @@ exports[`should render internal actions 1`] = ` api </h2> </header> - <div - className="web-api-domain-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "API Desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="API Desc" + > + <div + className="web-api-domain-description markdown" + /> + </SafeHTMLInjection> <div className="web-api-domain-actions" > @@ -317,14 +312,13 @@ exports[`should render only actions matching the query 1`] = ` api </h2> </header> - <div - className="web-api-domain-description markdown" - dangerouslySetInnerHTML={ - { - "__html": "API Desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="API Desc" + > + <div + className="web-api-domain-description markdown" + /> + </SafeHTMLInjection> <div className="web-api-domain-actions" > diff --git a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Params-test.tsx.snap b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Params-test.tsx.snap index c38b355494b..338fc2de1f2 100644 --- a/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Params-test.tsx.snap +++ b/server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Params-test.tsx.snap @@ -44,14 +44,13 @@ exports[`should render deprecated and internal parameters 1`] = ` </div> </td> <td> - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "Foo desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="Foo desc" + > + <div + className="markdown" + /> + </SafeHTMLInjection> </td> <td style={ @@ -94,14 +93,13 @@ exports[`should render deprecated and internal parameters 1`] = ` </div> </td> <td> - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "Foo desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="Foo desc" + > + <div + className="markdown" + /> + </SafeHTMLInjection> </td> <td style={ @@ -163,14 +161,13 @@ exports[`should render deprecated key 1`] = ` </div> </td> <td> - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "Foo desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="Foo desc" + > + <div + className="markdown" + /> + </SafeHTMLInjection> </td> <td style={ @@ -212,14 +209,13 @@ exports[`should render different value constraints 1`] = ` </div> </td> <td> - <div - className="markdown" - dangerouslySetInnerHTML={ - { - "__html": "Foo desc", - } - } - /> + <SafeHTMLInjection + htmlAsString="Foo desc" + > + <div + className="markdown" + /> + </SafeHTMLInjection> </td> <td style={ diff --git a/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx b/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx index 1de9715fa03..21f4275f092 100644 --- a/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx +++ b/server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.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 * as React from 'react'; import { dismissAnalysisWarning, getTask } from '../../api/ce'; import withCurrentUserContext from '../../app/components/current-user/withCurrentUserContext'; @@ -25,7 +26,7 @@ import Modal from '../../components/controls/Modal'; import WarningIcon from '../../components/icons/WarningIcon'; import DeferredSpinner from '../../components/ui/DeferredSpinner'; import { translate } from '../../helpers/l10n'; -import { sanitizeStringRestricted } from '../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../helpers/sanitize'; import { TaskWarning } from '../../types/tasks'; import { CurrentUser } from '../../types/users'; @@ -135,11 +136,9 @@ export class AnalysisWarningsModal extends React.PureComponent<Props, State> { <div className="panel panel-vertical" key={key}> <WarningIcon className="pull-left spacer-right" /> <div className="overflow-hidden markdown"> - <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} /> {dismissable && currentUser.isLoggedIn && ( diff --git a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap index 2f0e5304c2f..3b834d7ec35 100644 --- a/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap @@ -28,12 +28,9 @@ exports[`should fetch task warnings if it has to 1`] = ` <div className="overflow-hidden markdown" > - <span - dangerouslySetInnerHTML={ - { - "__html": "message foo", - } - } + <SafeHTMLInjection + htmlAsString="message foo" + sanitizeLevel={3} /> </div> </div> @@ -47,12 +44,9 @@ exports[`should fetch task warnings if it has to 1`] = ` <div className="overflow-hidden markdown" > - <span - dangerouslySetInnerHTML={ - { - "__html": "message-bar", - } - } + <SafeHTMLInjection + htmlAsString="message-bar" + sanitizeLevel={3} /> </div> </div> @@ -68,12 +62,9 @@ secondline <div className="overflow-hidden markdown" > - <span - dangerouslySetInnerHTML={ - { - "__html": "multiline message<br>secondline<br> third line", - } - } + <SafeHTMLInjection + htmlAsString="multiline message<br />secondline<br /> third line" + sanitizeLevel={3} /> </div> </div> @@ -120,12 +111,9 @@ exports[`should render correctly: default 1`] = ` <div className="overflow-hidden markdown" > - <span - dangerouslySetInnerHTML={ - { - "__html": "warning 1", - } - } + <SafeHTMLInjection + htmlAsString="warning 1" + sanitizeLevel={3} /> </div> </div> @@ -139,12 +127,9 @@ exports[`should render correctly: default 1`] = ` <div className="overflow-hidden markdown" > - <span - dangerouslySetInnerHTML={ - { - "__html": "warning 2", - } - } + <SafeHTMLInjection + htmlAsString="warning 2" + sanitizeLevel={3} /> </div> </div> @@ -191,12 +176,9 @@ exports[`should render correctly: do not show dismissable links for anonymous 1` <div className="overflow-hidden markdown" > - <span - dangerouslySetInnerHTML={ - { - "__html": "Lorem ipsum", - } - } + <SafeHTMLInjection + htmlAsString="Lorem ipsum" + sanitizeLevel={3} /> </div> </div> @@ -243,12 +225,9 @@ exports[`should render correctly: with dismissable warnings 1`] = ` <div className="overflow-hidden markdown" > - <span - dangerouslySetInnerHTML={ - { - "__html": "Lorem ipsum", - } - } + <SafeHTMLInjection + htmlAsString="Lorem ipsum" + sanitizeLevel={3} /> <div className="spacer-top display-flex-inline" diff --git a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx index 3b9bd31ed5e..0030a3224e1 100644 --- a/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx +++ b/server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx @@ -17,12 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import * as React from 'react'; import { DeleteButton, EditButton } from '../../../components/controls/buttons'; import Toggler from '../../../components/controls/Toggler'; import { PopupPlacement } from '../../../components/ui/popups'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { IssueComment } from '../../../types/types'; import DateFromNow from '../../intl/DateFromNow'; import Avatar from '../../ui/Avatar'; @@ -95,11 +96,11 @@ export default class IssueCommentLine extends React.PureComponent<Props, State> /> {displayName} </div> - <div - className="issue-comment-text markdown" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeUserInput(comment.htmlText) }} - /> + + <SafeHTMLInjection htmlAsString={comment.htmlText} sanitizeLevel={SanitizeLevel.USER_INPUT}> + <div className="issue-comment-text markdown" /> + </SafeHTMLInjection> + <div className="issue-comment-age"> <span className="a11y-hidden">{translate('issue.comment.posted_on')}</span> <DateFromNow date={comment.createdAt} /> diff --git a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.tsx.snap b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.tsx.snap index 563f86aac29..e0449f31e1a 100644 --- a/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.tsx.snap +++ b/server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.tsx.snap @@ -28,14 +28,14 @@ exports[`should open the right popups when the buttons are clicked 3`] = ` /> John Doe </div> - <div - className="issue-comment-text markdown" - dangerouslySetInnerHTML={ - { - "__html": "<b>test</b>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<b>test</b>" + sanitizeLevel={2} + > + <div + className="issue-comment-text markdown" + /> + </SafeHTMLInjection> <div className="issue-comment-age" > @@ -126,14 +126,14 @@ exports[`should render correctly a comment that is not updatable 1`] = ` /> John Doe </div> - <div - className="issue-comment-text markdown" - dangerouslySetInnerHTML={ - { - "__html": "<b>test</b>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<b>test</b>" + sanitizeLevel={2} + > + <div + className="issue-comment-text markdown" + /> + </SafeHTMLInjection> <div className="issue-comment-age" > @@ -168,14 +168,14 @@ exports[`should render correctly a comment that is updatable 1`] = ` /> John Doe </div> - <div - className="issue-comment-text markdown" - dangerouslySetInnerHTML={ - { - "__html": "<b>test</b>", - } - } - /> + <SafeHTMLInjection + htmlAsString="<b>test</b>" + sanitizeLevel={2} + > + <div + className="issue-comment-text markdown" + /> + </SafeHTMLInjection> <div className="issue-comment-age" > diff --git a/server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx b/server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx index 5a6d02df966..de46272ae49 100644 --- a/server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx +++ b/server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx @@ -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 * as React from 'react'; import { translate, translateWithParameters } from '../../../helpers/l10n'; -import { sanitizeUserInput } from '../../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../../helpers/sanitize'; import { IssueComment } from '../../../types/types'; import { DeleteButton, EditButton } from '../../controls/buttons'; import DateTimeFormatter from '../../intl/DateTimeFormatter'; @@ -81,12 +82,14 @@ export default class CommentTile extends React.PureComponent<CommentTileProps, C </div> <div className="spacer-top display-flex-space-between"> {!showEditArea && ( - <div - className="flex-1 markdown" - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ __html: sanitizeUserInput(comment.htmlText) }} - /> + <SafeHTMLInjection + htmlAsString={comment.htmlText} + sanitizeLevel={SanitizeLevel.USER_INPUT} + > + <div className="flex-1 markdown" /> + </SafeHTMLInjection> )} + {showEditArea && ( <div className="flex-1"> <CommentForm 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 5a884eb9784..8bce3f6476a 100644 --- a/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx +++ b/server/sonar-web/src/main/js/components/rules/RuleDescription.tsx @@ -17,12 +17,13 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import classNames from 'classnames'; import * as React from 'react'; import { RuleDescriptionSection } from '../../apps/coding-rules/rule'; import applyCodeDifferences from '../../helpers/code-difference'; import { translate, translateWithParameters } from '../../helpers/l10n'; -import { sanitizeString } from '../../helpers/sanitize'; +import { SafeHTMLInjection, SanitizeLevel } from '../../helpers/sanitize'; import ButtonToggle from '../controls/ButtonToggle'; import { Alert } from '../ui/Alert'; import OtherContextOption from './OtherContextOption'; @@ -157,13 +158,16 @@ export default class RuleDescription extends React.PureComponent<Props, State> { </h2> )} </div> + {selectedContext.key === OTHERS_KEY ? ( <OtherContextOption /> ) : ( - <div - /* eslint-disable-next-line react/no-danger */ - dangerouslySetInnerHTML={{ __html: sanitizeString(selectedContext.content) }} - /> + <SafeHTMLInjection + htmlAsString={selectedContext.content} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <div /> + </SafeHTMLInjection> )} </div> </div> @@ -171,19 +175,20 @@ export default class RuleDescription extends React.PureComponent<Props, State> { } return ( - <div - className={classNames(className, { - markdown: isDefault, - 'rule-desc': !isDefault, - })} - ref={(node) => { - applyCodeDifferences(node); - }} - // eslint-disable-next-line react/no-danger - dangerouslySetInnerHTML={{ - __html: sanitizeString(sections[0].content), - }} - /> + <SafeHTMLInjection + htmlAsString={sections[0].content} + sanitizeLevel={SanitizeLevel.FORBID_SVG_MATHML} + > + <div + className={classNames(className, { + markdown: isDefault, + 'rule-desc': !isDefault, + })} + ref={(node) => { + applyCodeDifferences(node); + }} + /> + </SafeHTMLInjection> ); } } 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 41c26fef1c8..6e20dac6adc 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 @@ -17,9 +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 { render } from '@testing-library/react'; import React from 'react'; import applyCodeDifferences from '../code-difference'; +import { SafeHTMLInjection } from '../sanitize'; it('should apply diff view correctly', () => { const { container } = renderDom(properCodeSnippet); @@ -146,12 +148,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 3239b72ae01..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/__tests__/sanitize-test.tsx b/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.tsx new file mode 100644 index 00000000000..e3990750bbc --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.tsx @@ -0,0 +1,295 @@ +/* + * 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 React from '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/src/main/js/helpers/code-difference.ts b/server/sonar-web/src/main/js/helpers/code-difference.ts index 2c07ef2679f..7fcdadd0d43 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 { diffLines } from 'diff'; import { groupBy, keyBy } from 'lodash'; -import { sanitizeString } from './sanitize'; +import { sanitizeHTMLNoSVGNoMathML } from './sanitize'; const NUMBER_OF_EXAMPLES = 2; @@ -71,7 +72,8 @@ function differentiateCode(compliant: string, nonCompliant: string) { let compliantCode = ''; hunks.forEach((hunk) => { - const value = sanitizeString(hunk.value); + const value = sanitizeHTMLNoSVGNoMathML(hunk.value); + if (!hunk.added && !hunk.removed) { nonCompliantCode += `<div>${value}</div>`; compliantCode += `<div>${value}</div>`; 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/src/main/js/helpers/sanitize.tsx b/server/sonar-web/src/main/js/helpers/sanitize.tsx new file mode 100644 index 00000000000..8755c5da014 --- /dev/null +++ b/server/sonar-web/src/main/js/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) }, + }); |