From 9b93aa5e758e6469711317ecc55728272e14de6e Mon Sep 17 00:00:00 2001 From: Wouter Admiraal Date: Fri, 23 Apr 2021 15:55:40 +0200 Subject: [PATCH] SONAR-14670 Wrap calls to dompurify --- .../js/apps/about/components/AboutApp.tsx | 4 +- .../components/ActivationFormModal.tsx | 14 +- .../components/CustomRuleFormModal.tsx | 14 +- .../components/RuleDetailsDescription.tsx | 8 +- .../components/RuleDetailsParameters.tsx | 12 +- .../__tests__/ActivationFormModal-test.tsx | 54 +- .../__tests__/CustomRuleFormModal-test.tsx | 15 +- .../ActivationFormModal-test.tsx.snap | 587 +++++++++++++++++- .../CustomRuleFormModal-test.tsx.snap | 98 ++- .../components/HotspotReviewHistory.tsx | 8 +- .../components/HotspotViewerTabs.tsx | 5 +- .../js/apps/settings/__tests__/utils-test.ts | 105 +--- .../apps/settings/components/Definition.tsx | 7 +- .../components/SubCategoryDefinitionsList.tsx | 8 +- .../src/main/js/apps/settings/utils.ts | 7 - .../common/AnalysisWarningsModal.tsx | 6 +- .../issue/components/IssueCommentLine.tsx | 5 +- .../js/helpers/__tests__/sanitize-test.ts | 161 +++++ .../sonar-web/src/main/js/helpers/sanitize.ts | 32 + .../src/main/js/helpers/testMocks.ts | 11 + 20 files changed, 995 insertions(+), 166 deletions(-) create mode 100644 server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts create mode 100644 server/sonar-web/src/main/js/helpers/sanitize.ts diff --git a/server/sonar-web/src/main/js/apps/about/components/AboutApp.tsx b/server/sonar-web/src/main/js/apps/about/components/AboutApp.tsx index b7d899a1f0f..652f712e689 100644 --- a/server/sonar-web/src/main/js/apps/about/components/AboutApp.tsx +++ b/server/sonar-web/src/main/js/apps/about/components/AboutApp.tsx @@ -17,7 +17,6 @@ * 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'; import { Location } from 'history'; import { keyBy } from 'lodash'; import * as React from 'react'; @@ -31,6 +30,7 @@ import A11ySkipTarget from '../../../app/components/a11y/A11ySkipTarget'; import withIndexationContext, { WithIndexationContextProps } from '../../../components/hoc/withIndexationContext'; +import { sanitizeString } from '../../../helpers/sanitize'; import { getAppState, getCurrentUser, @@ -163,7 +163,7 @@ export class AboutApp extends React.PureComponent {
)} 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 51470ba11e3..65d00e2a7c1 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 @@ -18,7 +18,6 @@ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ import * as classNames from 'classnames'; -import { sanitize } from 'dompurify'; import * as React from 'react'; import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; import Modal from 'sonar-ui-common/components/controls/Modal'; @@ -28,6 +27,7 @@ import { translate } from 'sonar-ui-common/helpers/l10n'; import { activateRule, Profile } from '../../../api/quality-profiles'; import SeverityHelper from '../../../components/shared/SeverityHelper'; import { SEVERITIES } from '../../../helpers/constants'; +import { sanitizeString } from '../../../helpers/sanitize'; import { sortProfiles } from '../../quality-profiles/utils'; interface Props { @@ -222,11 +222,13 @@ export default class ActivationFormModal extends React.PureComponent )} -
+ {param.htmlDesc !== undefined && ( +
+ )}
)) )} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/CustomRuleFormModal.tsx index 738187b2be7..d21c7e83498 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,7 +17,6 @@ * 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'; import * as React from 'react'; import { ResetButtonLink, SubmitButton } from 'sonar-ui-common/components/controls/buttons'; import Modal from 'sonar-ui-common/components/controls/Modal'; @@ -33,6 +32,7 @@ import FormattingTips from '../../../components/common/FormattingTips'; import SeverityHelper from '../../../components/shared/SeverityHelper'; import TypeHelper from '../../../components/shared/TypeHelper'; import { RULE_STATUSES, RULE_TYPES, SEVERITIES } from '../../../helpers/constants'; +import { sanitizeString } from '../../../helpers/sanitize'; interface Props { customRule?: T.RuleDetails; @@ -304,11 +304,13 @@ export default class CustomRuleFormModal extends React.PureComponent )} -
+ {param.htmlDesc !== undefined && ( +
+ )}
); diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsDescription.tsx index 8e1219a8d84..d89059fa468 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,12 +17,12 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ -import { sanitize } from 'dompurify'; import * as React from 'react'; import { Button, ResetButtonLink } from 'sonar-ui-common/components/controls/buttons'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; import { updateRule } from '../../../api/rules'; import FormattingTips from '../../../components/common/FormattingTips'; +import { sanitizeString } from '../../../helpers/sanitize'; import RemoveExtendedDescriptionModal from './RemoveExtendedDescriptionModal'; interface Props { @@ -112,7 +112,7 @@ export default class RuleDetailsDescription extends React.PureComponent )} {this.props.canWrite && ( @@ -190,11 +190,11 @@ export default class RuleDetailsDescription extends React.PureComponent - {hasDescription ? ( + {hasDescription && ruleDetails.htmlDesc !== undefined ? (
) : (
diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/RuleDetailsParameters.tsx index c134954aa05..91cf2b95cd4 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,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 { sanitize } from 'dompurify'; import * as React from 'react'; import { translate } from 'sonar-ui-common/helpers/l10n'; +import { sanitizeString } from '../../../helpers/sanitize'; interface Props { params: T.RuleParameter[]; @@ -30,10 +30,12 @@ export default class RuleDetailsParameters extends React.PureComponent { {param.key} -

+ {param.htmlDesc !== undefined && ( +

+ )} {param.defaultValue !== undefined && (

{translate('coding_rules.parameters.default_value')} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx index e011e79d46f..2f28c908ec2 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/ActivationFormModal-test.tsx @@ -17,21 +17,51 @@ * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ + import { shallow } from 'enzyme'; import * as React from 'react'; -import { mockQualityProfile, mockRule } from '../../../../helpers/testMocks'; +import { + mockQualityProfile, + mockRule, + mockRuleActivation, + mockRuleDetails, + mockRuleDetailsParameter +} from '../../../../helpers/testMocks'; import ActivationFormModal from '../ActivationFormModal'; -it('render correctly', () => { +it('should render correctly', () => { + expect(shallowRender()).toMatchSnapshot('default'); expect( - shallow( - - ) - ).toMatchSnapshot(); + shallowRender({ + profiles: [ + mockQualityProfile(), + mockQualityProfile({ depth: 2, actions: { edit: true }, language: 'js' }) + ] + }) + ).toMatchSnapshot('with deep profiles'); + expect(shallowRender({ rule: mockRuleDetails({ templateKey: 'foobar' }) })).toMatchSnapshot( + 'custom rule' + ); + expect(shallowRender({ activation: mockRuleActivation() })).toMatchSnapshot('update mode'); + const wrapper = shallowRender(); + wrapper.setState({ submitting: true }); + expect(wrapper).toMatchSnapshot('submitting'); }); + +function shallowRender(props: Partial = {}) { + return shallow( + + ); +} diff --git a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx index eb49f339c08..895e063555e 100644 --- a/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx +++ b/server/sonar-web/src/main/js/apps/coding-rules/components/__tests__/CustomRuleFormModal-test.tsx @@ -21,13 +21,13 @@ import { shallow } from 'enzyme'; import * as React from 'react'; import { submit, waitAndUpdate } from 'sonar-ui-common/helpers/testUtils'; import { createRule } from '../../../../api/rules'; -import { mockRule } from '../../../../helpers/testMocks'; +import { mockRule, mockRuleDetailsParameter } from '../../../../helpers/testMocks'; import CustomRuleFormModal from '../CustomRuleFormModal'; jest.mock('../../../../api/rules', () => ({ createRule: jest.fn() })); it('should render correctly', () => { - expect(shallowRender()).toMatchSnapshot(); + expect(shallowRender()).toMatchSnapshot('default'); }); it('should handle re-activation', async () => { @@ -43,7 +43,16 @@ function shallowRender(props: Partial = {}) { ); 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 2225b883507..2adb2cf8b78 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 @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`render correctly 1`] = ` +exports[`should render correctly: custom rule 1`] = `
+
+

+ coding_rules.custom_rule.activation_notice +

+
+
+
+ + coding_rules.activate + + + cancel + +
+ + +`; + +exports[`should render correctly: default 1`] = ` + +
+
+

+ title +

+
+
+ + coding_rules.active_in_all_profiles + +
+ + +
+
+ + +
+
+
+ + --> - - - - - - - section in HTML ]]> - - - Bar`); - - expect(clean.replace(/\s+/g, '')).toBe( - `Clickyalert("<b>hi</b>");<divid=notarealtagonclick=notcode()><notatag<<<allinonetextblock>>><%#somephpcodeherewrite("$horriblySyntacticConstruct1");%>*/alert('hi');*/alert('hi');-->*/alert('hi');-->'}-->--><!--Zoicks-->sectioninHTML]]>Bar` - ); - }); -}); diff --git a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx index 66a0d15c5f9..fadd728a2cf 100644 --- a/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx +++ b/server/sonar-web/src/main/js/apps/settings/components/Definition.tsx @@ -23,6 +23,7 @@ import { connect } from 'react-redux'; import AlertErrorIcon from 'sonar-ui-common/components/icons/AlertErrorIcon'; import AlertSuccessIcon from 'sonar-ui-common/components/icons/AlertSuccessIcon'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { sanitizeStringRestricted } from '../../../helpers/sanitize'; import { getSettingsAppChangedValue, getSettingsAppValidationMessage, @@ -36,8 +37,7 @@ import { getPropertyDescription, getPropertyName, getSettingValue, - isDefaultOrInherited, - sanitizeTranslation + isDefaultOrInherited } from '../utils'; import DefinitionActions from './DefinitionActions'; import Input from './inputs/Input'; @@ -154,7 +154,8 @@ export class Definition extends React.PureComponent { {description && (
)} diff --git a/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx b/server/sonar-web/src/main/js/apps/settings/components/SubCategoryDefinitionsList.tsx index 22493ed1046..62e9034f9fc 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 @@ -19,8 +19,9 @@ */ import { groupBy, isEqual, sortBy } from 'lodash'; import * as React from 'react'; +import { sanitizeStringRestricted } from '../../../helpers/sanitize'; import { Setting, SettingCategoryDefinition } from '../../../types/settings'; -import { getSubCategoryDescription, getSubCategoryName, sanitizeTranslation } from '../utils'; +import { getSubCategoryDescription, getSubCategoryName } from '../utils'; import DefinitionsList from './DefinitionsList'; import EmailForm from './EmailForm'; @@ -79,7 +80,10 @@ export default class SubCategoryDefinitionsList extends React.PureComponent )} { '), { - ALLOWED_ATTR: ['target', 'href'] - }) + __html: sanitizeStringRestricted(message.trim().replace(/\n/g, '
')) }} /> 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 84a617ccd88..06d4a937ed2 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,13 +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 { sanitize } from 'dompurify'; import * as React from 'react'; import { DeleteButton, EditButton } from 'sonar-ui-common/components/controls/buttons'; import Toggler from 'sonar-ui-common/components/controls/Toggler'; import DateFromNow from 'sonar-ui-common/components/intl/DateFromNow'; import { PopupPlacement } from 'sonar-ui-common/components/ui/popups'; import { translate, translateWithParameters } from 'sonar-ui-common/helpers/l10n'; +import { sanitizeString } from '../../../helpers/sanitize'; import Avatar from '../../ui/Avatar'; import CommentDeletePopup from '../popups/CommentDeletePopup'; import CommentPopup from '../popups/CommentPopup'; @@ -96,7 +96,8 @@ export default class IssueCommentLine extends React.PureComponent
{translate('issue.comment.posted_on')} 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 new file mode 100644 index 00000000000..1d906672562 --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/__tests__/sanitize-test.ts @@ -0,0 +1,161 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { sanitizeString, sanitizeStringRestricted } from '../sanitize'; + +describe('sanitizeStringRestricted', () => { + it('should preserve only specific formatting tags', () => { + expect( + sanitizeStringRestricted(` + Hi this is in italics and
    +
  • lists
  • +
  • are allowed
  • +
+

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

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

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

+ `); + }); + + /* + * Test code borrowed from OWASP's sanitizer tests + * https://github.com/OWASP/java-html-sanitizer/blob/master/src/test/resources/org/owasp/html/htmllexerinput1.html + */ + it('should strip everything else', () => { + const clean = sanitizeStringRestricted(` + + + + + + + + Test File For HtmlLexer & HtmlParser + + + + + + + +
+ Clicky +
+ + + alert("<b>hi</b>"); + + +
<div id=notarealtag onclick=notcode()>
+ + + + < notatag + + + + + + < < < all in one text block > > > + + Make sure that <!-- comments don't obscure the xmp close + <% # some php code here + write("
$horriblySyntacticConstruct1
\n\n"); + %> + + + */alert('hi'); + */alert('hi');--> + + <!--/* */alert('hi');--> + + ' } --> + --> + --> + + + + + + + section in HTML ]]> + + + Bar`); + + expect(clean.replace(/\s+/g, '')).toBe( + `Clickyalert("<b>hi</b>");<divid=notarealtagonclick=notcode()><notatag<<<allinonetextblock>>><%#somephpcodeherewrite("$horriblySyntacticConstruct1");%>*/alert('hi');*/alert('hi');-->*/alert('hi');-->'}-->--><!--Zoicks-->sectioninHTML]]>Bar` + ); + }); +}); + +describe('sanitizeString', () => { + it('should not allow MathML and SVG', () => { + const tainted = ` + Hi this is in italics and
    +
  • lists
  • +
  • are allowed
  • +
+

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

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

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

`; + + expect(sanitizeString(tainted).trimRight()).toBe(clean); + }); +}); diff --git a/server/sonar-web/src/main/js/helpers/sanitize.ts b/server/sonar-web/src/main/js/helpers/sanitize.ts new file mode 100644 index 00000000000..79b4a95fa4f --- /dev/null +++ b/server/sonar-web/src/main/js/helpers/sanitize.ts @@ -0,0 +1,32 @@ +/* + * SonarQube + * Copyright (C) 2009-2021 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 } }); +} diff --git a/server/sonar-web/src/main/js/helpers/testMocks.ts b/server/sonar-web/src/main/js/helpers/testMocks.ts index efc7915e897..5dbbc378f68 100644 --- a/server/sonar-web/src/main/js/helpers/testMocks.ts +++ b/server/sonar-web/src/main/js/helpers/testMocks.ts @@ -568,6 +568,17 @@ export function mockRule(overrides: Partial = {}): T.Rule { } as T.Rule; } +export function mockRuleActivation(overrides: Partial = {}): T.RuleActivation { + return { + createdAt: '2020-02-01', + inherit: 'NONE', + params: [{ key: 'foo', value: 'Bar' }], + qProfile: 'baz', + severity: 'MAJOR', + ...overrides + }; +} + export function mockRuleDetails(overrides: Partial = {}): T.RuleDetails { return { key: 'squid:S1337', -- 2.39.5