diff --git a/CHANGELOG.md b/CHANGELOG.md index fe22e2a8a..703c6410e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ # Change history for stripes-smart-components +## 9.0.3 In progress + +* Safely render user-provided markup in `` and `` components. Fixes STSMACOM-816. + ## [9.0.2](https://github.com/folio-org/stripes-smart-components/tree/v9.0.2) (2023-11-28) [Full Changelog](https://github.com/folio-org/stripes-smart-components/compare/v9.0.1...v9.0.2) * Send `X-Okapi-Token` header only when token is present in `stripes`. Refs STSMACOM-714. + ## [9.0.1](https://github.com/folio-org/stripes-smart-components/tree/v9.0.1) (2023-10-25) [Full Changelog](https://github.com/folio-org/stripes-smart-components/compare/v9.0.0...v9.0.1) diff --git a/lib/Notes/NoteViewPage/components/NoteView/NoteView.js b/lib/Notes/NoteViewPage/components/NoteView/NoteView.js index 041291857..5b7ede8d6 100644 --- a/lib/Notes/NoteViewPage/components/NoteView/NoteView.js +++ b/lib/Notes/NoteViewPage/components/NoteView/NoteView.js @@ -3,6 +3,8 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import get from 'lodash/get'; +import dompurify from 'dompurify'; + import { IfPermission, AppIcon, @@ -225,7 +227,7 @@ export default class NoteView extends Component { assigned, } = this.state.sections; - const noteContentMarkup = { __html: noteData.content }; + const noteContentMarkup = { __html: dompurify.sanitize(noteData.content) }; const paneTitle = noteData.title; diff --git a/lib/Notes/NoteViewPage/components/NoteView/tests/interactor.js b/lib/Notes/NoteViewPage/components/NoteView/tests/interactor.js index 333343a32..a737557ad 100644 --- a/lib/Notes/NoteViewPage/components/NoteView/tests/interactor.js +++ b/lib/Notes/NoteViewPage/components/NoteView/tests/interactor.js @@ -3,7 +3,8 @@ import { scoped, clickable, text, - isPresent + isPresent, + computed, } from '@bigtest/interactor'; import { AccordionInteractor } from '@folio/stripes-components/lib/Accordion/tests/interactor'; @@ -12,6 +13,12 @@ import NoValueInteractor from '@folio/stripes-components/lib/NoValue/tests/inter import ReferredRecordInteractor from '../../../../components/ReferredRecord/tests/interactor'; +function markup(selector) { + return computed(function () { + return this.$(selector).innerHTML; + }); +} + export default interactor(class NoteViewInteractor { static defaultScope = '[data-test-note-view]'; @@ -26,4 +33,5 @@ export default interactor(class NoteViewInteractor { metaSection = MetaSectionInteractor(); hasEmptyNoteType = isPresent(`[data-test-note-view-note-type] ${NoValueInteractor.defaultScope}`); hasEmptyNoteTitle = isPresent(`[data-test-note-view-note-title] ${NoValueInteractor.defaultScope}`); + noteContentText = markup('[data-test-note-view-note-details]'); }); diff --git a/lib/Notes/NoteViewPage/components/NoteView/tests/note-view-test.js b/lib/Notes/NoteViewPage/components/NoteView/tests/note-view-test.js index 53ab53f7a..90f623327 100644 --- a/lib/Notes/NoteViewPage/components/NoteView/tests/note-view-test.js +++ b/lib/Notes/NoteViewPage/components/NoteView/tests/note-view-test.js @@ -31,6 +31,18 @@ const noteData = { type: 'General note', }; +const noteWithContentData = { + ...noteData, + content: 'Message is rendered', +}; + +const noteWithSusContentData = { + ...noteData, + content: 'Message is renderedlink', +}; + +const cleanContent = 'Message is renderedlink'; + describe('NoteView', () => { const noteView = new NoteViewInteractor(); @@ -112,4 +124,44 @@ describe('NoteView', () => { expect(noteView.insertedReferredRecord.isPresent).to.be.true; }); }); + + describe('rendering NoteView component with content', () => { + setupApplication(); + + beforeEach(async () => { + await mount( +
Test
} + /> + ); + }); + + it('should display note content', () => { + expect(noteView.noteContentText).to.equal(noteWithContentData.content); + }); + }); + + describe('rendering NoteView component with suspicious content', () => { + setupApplication(); + + beforeEach(async () => { + await mount( +
Test
} + /> + ); + }); + + it('should screen suspicious content for display', () => { + expect(noteView.noteContentText).to.equal(cleanContent); + }); + }); }); diff --git a/lib/Notes/components/NotesAccordion/components/NotesList/NotesList.js b/lib/Notes/components/NotesAccordion/components/NotesList/NotesList.js index a818b35bb..670b677a0 100644 --- a/lib/Notes/components/NotesAccordion/components/NotesList/NotesList.js +++ b/lib/Notes/components/NotesAccordion/components/NotesList/NotesList.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import dompurify from 'dompurify'; import { FormattedMessage, FormattedDate, @@ -126,9 +127,9 @@ class NotesList extends React.Component { ); - const htmlString = isDetailsExpanded[id] + const htmlString = dompurify.sanitize(isDetailsExpanded[id] ? content - : getHTMLSubstring(content, DETAILS_CUTOFF_LENGTH); + : getHTMLSubstring(content, DETAILS_CUTOFF_LENGTH)); const detailsContent = content && (
diff --git a/package.json b/package.json index 75035a3d3..cfe01437a 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "dependencies": { "@rehooks/local-storage": "^2.4.4", "classnames": "^2.2.6", + "dompurify": "^3.0.9", "final-form": "^4.18.2", "final-form-arrays": "^3.0.2", "lodash": "^4.17.4", diff --git a/yarn.lock b/yarn.lock index 28418d966..d35d20e8b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4872,6 +4872,11 @@ domutils@^1.7.0: dom-serializer "0" domelementtype "1" +dompurify@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.9.tgz#b3f362f24b99f53498c75d43ecbd784b0b3ad65e" + integrity sha512-uyb4NDIvQ3hRn6NiC+SIFaP4mJ/MdXlvtunaqK9Bn6dD3RuB/1S/gasEjDHD8eiaqdSael2vBv+hOs7Y+jhYOQ== + domutils@^2.5.2, domutils@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"