Skip to content

Commit e47fb2e

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
fix: remove setting unsafe innerHTML
As it is vulnerable to stored Cross-Site Scripting. Ref: PNX-3669 Signed-off-by: Akos Kitta <[email protected]>
1 parent ee43a12 commit e47fb2e

File tree

3 files changed

+102
-2
lines changed

3 files changed

+102
-2
lines changed

Diff for: arduino-ide-extension/src/browser/library/library-list-widget.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { Installable } from '../../common/protocol';
2020
import { ListItemRenderer } from '../widgets/component-list/list-item-renderer';
2121
import { nls } from '@theia/core/lib/common';
2222
import { LibraryFilterRenderer } from '../widgets/component-list/filter-renderer';
23-
import { findChildTheiaButton } from '../utils/dom';
23+
import { findChildTheiaButton, splitByBoldTag } from '../utils/dom';
2424

2525
@injectable()
2626
export class LibraryListWidget extends ListWidget<
@@ -81,7 +81,7 @@ export class LibraryListWidget extends ListWidget<
8181
let installDependencies: boolean | undefined = undefined;
8282
if (dependencies.length) {
8383
const message = document.createElement('div');
84-
message.innerHTML =
84+
const textContent =
8585
dependencies.length === 1
8686
? nls.localize(
8787
'arduino/library/needsOneDependency',
@@ -95,6 +95,22 @@ export class LibraryListWidget extends ListWidget<
9595
item.name,
9696
version
9797
);
98+
const segments = splitByBoldTag(textContent);
99+
if (!segments) {
100+
message.textContent = textContent;
101+
} else {
102+
segments.map((segment) => {
103+
const span = document.createElement('span');
104+
if (typeof segment === 'string') {
105+
span.textContent = segment;
106+
} else {
107+
const bold = document.createElement('b');
108+
bold.textContent = segment.textContent;
109+
span.appendChild(bold);
110+
}
111+
message.appendChild(span);
112+
});
113+
}
98114
const listContainer = document.createElement('div');
99115
listContainer.style.maxHeight = '300px';
100116
listContainer.style.overflowY = 'auto';

Diff for: arduino-ide-extension/src/browser/utils/dom.ts

+32
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,35 @@ export function findChildTheiaButton(
3535
function isHTMLElement(element: Element): element is HTMLElement {
3636
return element instanceof HTMLElement;
3737
}
38+
39+
type Segment = string | { textContent: string; bold: true };
40+
/**
41+
* Returns with an array of `Segments` by splitting raw HTML text on the `<b></b>` groups. If splitting is not possible, returns `undefined`.
42+
* Example: `one<b>two</b>three<b>four</b>five` will provide an five element length array. Where the 1<sup>st</sup> and 3<sup>rd</sup> elements are objects and the rest are string.
43+
*/
44+
export function splitByBoldTag(text: string): Segment[] | undefined {
45+
const matches = text.matchAll(new RegExp(/<\s*b[^>]*>(.*?)<\s*\/\s*b>/gm));
46+
if (!matches) {
47+
return undefined;
48+
}
49+
const segments: Segment[] = [];
50+
const textLength = text.length;
51+
let processedLength = 0;
52+
for (const match of matches) {
53+
const { index } = match;
54+
if (typeof index === 'number') {
55+
if (!segments.length && index) {
56+
segments.push(text.substring(0, index));
57+
}
58+
if (processedLength > 0) {
59+
segments.push(text.substring(processedLength, index));
60+
}
61+
segments.push({ textContent: match[1], bold: true });
62+
processedLength = index + match[0].length;
63+
}
64+
}
65+
if (segments.length && textLength > processedLength) {
66+
segments.push(text.substring(processedLength));
67+
}
68+
return segments.length ? segments : undefined;
69+
}

Diff for: arduino-ide-extension/src/test/browser/dom.test.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { splitByBoldTag } from '../../browser/utils/dom';
2+
import { expect } from 'chai';
3+
4+
describe('dom', () => {
5+
describe('splitByBoldTag', () => {
6+
it('should split by bold tags', () => {
7+
const actual = splitByBoldTag('one<b>matchOne</b>two');
8+
const expected = ['one', { textContent: 'matchOne', bold: true }, 'two'];
9+
expect(actual).to.be.deep.equal(expected);
10+
});
11+
12+
it('should handle starting bold tags', () => {
13+
const actual = splitByBoldTag(
14+
'<b>matchOne</b>one<b>matchTwo</b> two <b>matchThree</b> three'
15+
);
16+
const expected = [
17+
{ textContent: 'matchOne', bold: true },
18+
'one',
19+
{ textContent: 'matchTwo', bold: true },
20+
' two ',
21+
{ textContent: 'matchThree', bold: true },
22+
' three',
23+
];
24+
expect(actual).to.be.deep.equal(expected);
25+
});
26+
27+
it('should handle unclosed bold tags', () => {
28+
const actual = splitByBoldTag(
29+
'<b>matchOne</b>one<b>matchTwo</b> two <b>matchThree</b> three <b> '
30+
);
31+
const expected = [
32+
{ textContent: 'matchOne', bold: true },
33+
'one',
34+
{ textContent: 'matchTwo', bold: true },
35+
' two ',
36+
{ textContent: 'matchThree', bold: true },
37+
' three <b> ',
38+
];
39+
expect(actual).to.be.deep.equal(expected);
40+
});
41+
42+
it('should handle no matches', () => {
43+
const actual = splitByBoldTag('<b>alma');
44+
expect(actual).to.be.undefined;
45+
});
46+
47+
it('should handle empty strings', () => {
48+
const actual = splitByBoldTag('');
49+
expect(actual).to.be.undefined;
50+
});
51+
});
52+
});

0 commit comments

Comments
 (0)