From 9f4d9e9399381401255d1a2aecb60f2cb33dfdb2 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 11 May 2023 14:55:46 +0200 Subject: [PATCH] fix: remove setting unsafe `innerHTML` As it is vulnerable to stored Cross-Site Scripting. Ref: PNX-3669 Signed-off-by: Akos Kitta --- .../browser/library/library-list-widget.ts | 20 ++++++- .../src/browser/utils/dom.ts | 32 ++++++++++++ .../src/test/browser/dom.test.ts | 52 +++++++++++++++++++ 3 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 arduino-ide-extension/src/test/browser/dom.test.ts diff --git a/arduino-ide-extension/src/browser/library/library-list-widget.ts b/arduino-ide-extension/src/browser/library/library-list-widget.ts index 25480de95..690799923 100644 --- a/arduino-ide-extension/src/browser/library/library-list-widget.ts +++ b/arduino-ide-extension/src/browser/library/library-list-widget.ts @@ -20,7 +20,7 @@ import { Installable } from '../../common/protocol'; import { ListItemRenderer } from '../widgets/component-list/list-item-renderer'; import { nls } from '@theia/core/lib/common'; import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer'; -import { findChildTheiaButton } from '../utils/dom'; +import { findChildTheiaButton, splitByBoldTag } from '../utils/dom'; @injectable() export class LibraryListWidget extends ListWidget< @@ -81,7 +81,7 @@ export class LibraryListWidget extends ListWidget< let installDependencies: boolean | undefined = undefined; if (dependencies.length) { const message = document.createElement('div'); - message.innerHTML = + const textContent = dependencies.length === 1 ? nls.localize( 'arduino/library/needsOneDependency', @@ -95,6 +95,22 @@ export class LibraryListWidget extends ListWidget< item.name, version ); + const segments = splitByBoldTag(textContent); + if (!segments) { + message.textContent = textContent; + } else { + segments.map((segment) => { + const span = document.createElement('span'); + if (typeof segment === 'string') { + span.textContent = segment; + } else { + const bold = document.createElement('b'); + bold.textContent = segment.textContent; + span.appendChild(bold); + } + message.appendChild(span); + }); + } const listContainer = document.createElement('div'); listContainer.style.maxHeight = '300px'; listContainer.style.overflowY = 'auto'; diff --git a/arduino-ide-extension/src/browser/utils/dom.ts b/arduino-ide-extension/src/browser/utils/dom.ts index 938b496ab..47cc830e6 100644 --- a/arduino-ide-extension/src/browser/utils/dom.ts +++ b/arduino-ide-extension/src/browser/utils/dom.ts @@ -35,3 +35,35 @@ export function findChildTheiaButton( function isHTMLElement(element: Element): element is HTMLElement { return element instanceof HTMLElement; } + +type Segment = string | { textContent: string; bold: true }; +/** + * Returns with an array of `Segments` by splitting raw HTML text on the `` groups. If splitting is not possible, returns `undefined`. + * Example: `onetwothreefourfive` will provide an five element length array. Where the 1st and 3rd elements are objects and the rest are string. + */ +export function splitByBoldTag(text: string): Segment[] | undefined { + const matches = text.matchAll(new RegExp(/<\s*b[^>]*>(.*?)<\s*\/\s*b>/gm)); + if (!matches) { + return undefined; + } + const segments: Segment[] = []; + const textLength = text.length; + let processedLength = 0; + for (const match of matches) { + const { index } = match; + if (typeof index === 'number') { + if (!segments.length && index) { + segments.push(text.substring(0, index)); + } + if (processedLength > 0) { + segments.push(text.substring(processedLength, index)); + } + segments.push({ textContent: match[1], bold: true }); + processedLength = index + match[0].length; + } + } + if (segments.length && textLength > processedLength) { + segments.push(text.substring(processedLength)); + } + return segments.length ? segments : undefined; +} diff --git a/arduino-ide-extension/src/test/browser/dom.test.ts b/arduino-ide-extension/src/test/browser/dom.test.ts new file mode 100644 index 000000000..5106024cc --- /dev/null +++ b/arduino-ide-extension/src/test/browser/dom.test.ts @@ -0,0 +1,52 @@ +import { splitByBoldTag } from '../../browser/utils/dom'; +import { expect } from 'chai'; + +describe('dom', () => { + describe('splitByBoldTag', () => { + it('should split by bold tags', () => { + const actual = splitByBoldTag('onematchOnetwo'); + const expected = ['one', { textContent: 'matchOne', bold: true }, 'two']; + expect(actual).to.be.deep.equal(expected); + }); + + it('should handle starting bold tags', () => { + const actual = splitByBoldTag( + 'matchOneonematchTwo two matchThree three' + ); + const expected = [ + { textContent: 'matchOne', bold: true }, + 'one', + { textContent: 'matchTwo', bold: true }, + ' two ', + { textContent: 'matchThree', bold: true }, + ' three', + ]; + expect(actual).to.be.deep.equal(expected); + }); + + it('should handle unclosed bold tags', () => { + const actual = splitByBoldTag( + 'matchOneonematchTwo two matchThree three ' + ); + const expected = [ + { textContent: 'matchOne', bold: true }, + 'one', + { textContent: 'matchTwo', bold: true }, + ' two ', + { textContent: 'matchThree', bold: true }, + ' three ', + ]; + expect(actual).to.be.deep.equal(expected); + }); + + it('should handle no matches', () => { + const actual = splitByBoldTag('alma'); + expect(actual).to.be.undefined; + }); + + it('should handle empty strings', () => { + const actual = splitByBoldTag(''); + expect(actual).to.be.undefined; + }); + }); +});