Skip to content

Commit e47fb2e

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

File tree

3 files changed

+102
-2
lines changed

3 files changed

+102
-2
lines changed

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

Lines changed: 18 additions & 2 deletions
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';

arduino-ide-extension/src/browser/utils/dom.ts

Lines changed: 32 additions & 0 deletions
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+
}
Lines changed: 52 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy