aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/sonar-web/.eslintrc24
-rw-r--r--server/sonar-web/src/main/js/app/components/extensions/exposeLibraries.ts5
-rw-r--r--server/sonar-web/src/main/js/app/components/search/SearchResult.tsx11
-rw-r--r--server/sonar-web/src/main/js/app/components/search/__tests__/__snapshots__/SearchResult-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/ActivationFormModal.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx14
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx44
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/ActivationFormModal-test.tsx.snap64
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/CustomRuleFormModal-test.tsx.snap32
-rw-r--r--server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/__snapshots__/RuleDetailsParameters-test.tsx.snap26
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/HotspotReviewHistory.tsx12
-rw-r--r--server/sonar-web/src/main/js/apps/security-hotspots/components/__tests__/__snapshots__/HotspotReviewHistory-test.tsx.snap128
-rw-r--r--server/sonar-web/src/main/js/apps/sessions/components/Login.tsx11
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/DefinitionRenderer.tsx13
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx18
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/DefinitionRenderer-test.tsx.snap96
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/__tests__/__snapshots__/SubCategoryDefinitionsList-test.tsx.snap48
-rw-r--r--server/sonar-web/src/main/js/apps/settings/components/inputs/InputForFormattedText.tsx15
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/Action.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/Domain.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/Params.tsx10
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Action-test.tsx.snap15
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Domain-test.tsx.snap90
-rw-r--r--server/sonar-web/src/main/js/apps/web-api/components/__tests__/__snapshots__/Params-test.tsx.snap60
-rw-r--r--server/sonar-web/src/main/js/components/common/AnalysisWarningsModal.tsx11
-rw-r--r--server/sonar-web/src/main/js/components/common/__tests__/__snapshots__/AnalysisWarningsModal-test.tsx.snap63
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/IssueCommentLine.tsx13
-rw-r--r--server/sonar-web/src/main/js/components/issue/components/__tests__/__snapshots__/IssueCommentLine-test.tsx.snap48
-rw-r--r--server/sonar-web/src/main/js/components/issue/popups/CommentTile.tsx15
-rw-r--r--server/sonar-web/src/main/js/components/rules/RuleDescription.tsx41
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/code-difference-test.tsx12
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts160
-rw-r--r--server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.tsx295
-rw-r--r--server/sonar-web/src/main/js/helpers/code-difference.ts6
-rw-r--r--server/sonar-web/src/main/js/helpers/sanitize.ts57
-rw-r--r--server/sonar-web/src/main/js/helpers/sanitize.tsx124
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 &amp; HtmlParser</title>
- <link rel=stylesheet type="text/css" src=foo/bar.css />
- <body
- bgcolor=white
- linkcolor = "blue"
- onload="document.writeln(
- &quot;&lt;p&gt;properly escaped code in a handler&lt;/p&gt;&quot;);"
- >
-
- <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("&lt;b&gt;hi&lt;/b&gt;");</gxp:attr>
- </input>
-
- <pre>&lt;div id=notarealtag onclick=notcode()&gt;</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("&lt;b&gt;hi&lt;/b&gt;");&lt;divid=notarealtagonclick=notcode()&gt;&lt;notatag&lt;&lt;&lt;allinonetextblock&gt;&gt;&gt;&lt;%#somephpcodeherewrite("$horriblySyntacticConstruct1");%&gt;*/alert('hi');*/alert('hi');--&gt;*/alert('hi');--&gt;'}--&gt;--&gt;&lt;!--Zoicks--&gt;sectioninHTML]]&gt;<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 &amp; HtmlParser</title>
+<link rel=stylesheet type="text/css" src=foo/bar.css />
+<body
+ bgcolor=white
+ linkcolor = "blue"
+ onload="document.writeln(
+ &quot;&lt;p&gt;properly escaped code in a handler&lt;/p&gt;&quot;);"
+>
+
+<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("&lt;b&gt;hi&lt;/b&gt;");</gxp:attr>
+</input>
+
+<pre>&lt;div id=notarealtag onclick=notcode()&gt;</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("&lt;b&gt;hi&lt;/b&gt;");
+ <pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
+ &lt; notatag
+ &lt; &lt; &lt; all in one text block &gt; &gt; &gt;
+ &lt;% # some php code here
+ write("<pre>$horriblySyntacticConstruct1</pre>
+ ");
+ %&gt;
+ */alert('hi');
+ */alert('hi');--&gt;
+ */alert('hi');--&gt; ' } --&gt;
+ --&gt;
+ <textarea>&lt;!-- Zoicks </textarea>--&gt;
+ 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 ]]&gt;
+ <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("&lt;b&gt;hi&lt;/b&gt;");
+ <pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
+ &lt; notatag
+ &lt; &lt; &lt; all in one text block &gt; &gt; &gt;
+ &lt;% # some php code here
+ write("<pre>$horriblySyntacticConstruct1</pre>
+ ");
+ %&gt;
+ */alert('hi');
+ */alert('hi');--&gt;
+ */alert('hi');--&gt; ' } --&gt;
+ --&gt;
+ <textarea>&lt;!-- Zoicks </textarea>--&gt;
+ 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 ]]&gt;
+ <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("&lt;b&gt;hi&lt;/b&gt;");
+ <pre>&lt;div id=notarealtag onclick=notcode()&gt;</pre>
+ &lt; notatag
+ &lt; &lt; &lt; all in one text block &gt; &gt; &gt;
+ &lt;% # some php code here
+ write("<pre>$horriblySyntacticConstruct1</pre>
+ ");
+ %&gt;
+ */alert('hi');
+ */alert('hi');--&gt;
+ */alert('hi');--&gt; ' } --&gt;
+ --&gt;
+ &lt;!-- Zoicks --&gt;
+ 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 ]]&gt;
+ <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("&lt;b&gt;hi&lt;/b&gt;");
+ &lt;div id=notarealtag onclick=notcode()&gt;
+ &lt; notatag
+ &lt; &lt; &lt; all in one text block &gt; &gt; &gt;
+ &lt;% # some php code here
+ write("$horriblySyntacticConstruct1
+ ");
+ %&gt;
+ */alert('hi');
+ */alert('hi');--&gt;
+ */alert('hi');--&gt; ' } --&gt;
+ --&gt;
+ &lt;!-- Zoicks --&gt;
+ 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 ]]&gt;
+ <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) },
+ });