Skip to content

Commit 810018b

Browse files
committed
Desktop: Security: Fixes #6004: Prevent XSS in Goto Anything
1 parent e0bfa0d commit 810018b

File tree

6 files changed

+49
-39
lines changed

6 files changed

+49
-39
lines changed

.eslintignore

+3
Original file line numberDiff line numberDiff line change
@@ -1927,6 +1927,9 @@ packages/renderer/headerAnchor.js.map
19271927
packages/renderer/htmlUtils.d.ts
19281928
packages/renderer/htmlUtils.js
19291929
packages/renderer/htmlUtils.js.map
1930+
packages/renderer/htmlUtils.test.d.ts
1931+
packages/renderer/htmlUtils.test.js
1932+
packages/renderer/htmlUtils.test.js.map
19301933
packages/renderer/index.d.ts
19311934
packages/renderer/index.js
19321935
packages/renderer/index.js.map

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1917,6 +1917,9 @@ packages/renderer/headerAnchor.js.map
19171917
packages/renderer/htmlUtils.d.ts
19181918
packages/renderer/htmlUtils.js
19191919
packages/renderer/htmlUtils.js.map
1920+
packages/renderer/htmlUtils.test.d.ts
1921+
packages/renderer/htmlUtils.test.js
1922+
packages/renderer/htmlUtils.test.js.map
19201923
packages/renderer/index.d.ts
19211924
packages/renderer/index.js
19221925
packages/renderer/index.js.map

packages/app-desktop/plugins/GotoAnything.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const { mergeOverlappingIntervals } = require('@joplin/lib/ArrayUtils.js');
1919
import markupLanguageUtils from '../utils/markupLanguageUtils';
2020
import focusEditorIfEditorCommand from '@joplin/lib/services/commands/focusEditorIfEditorCommand';
2121
import Logger from '@joplin/lib/Logger';
22+
import { MarkupToHtml } from '@joplin/renderer';
2223

2324
const logger = Logger.create('GotoAnything');
2425

@@ -81,7 +82,7 @@ class Dialog extends React.PureComponent<Props, State> {
8182
private inputRef: any;
8283
private itemListRef: any;
8384
private listUpdateIID_: any;
84-
private markupToHtml_: any;
85+
private markupToHtml_: MarkupToHtml;
8586
private userCallback_: any = null;
8687

8788
constructor(props: Props) {

packages/lib/htmlUtils.ts

-35
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const urlUtils = require('./urlUtils.js');
22
const Entities = require('html-entities').AllHtmlEntities;
33
const htmlentities = new Entities().encode;
4-
const htmlparser2 = require('@joplin/fork-htmlparser2');
54
const { escapeHtml } = require('./string-utils.js');
65

76
// [\s\S] instead of . for multiline matching
@@ -138,40 +137,6 @@ class HtmlUtils {
138137
return output.join(' ');
139138
}
140139

141-
public stripHtml(html: string) {
142-
const output: string[] = [];
143-
144-
const tagStack: any[] = [];
145-
146-
const currentTag = () => {
147-
if (!tagStack.length) return '';
148-
return tagStack[tagStack.length - 1];
149-
};
150-
151-
const disallowedTags = ['script', 'style', 'head', 'iframe', 'frameset', 'frame', 'object', 'base'];
152-
153-
const parser = new htmlparser2.Parser({
154-
155-
onopentag: (name: string) => {
156-
tagStack.push(name.toLowerCase());
157-
},
158-
159-
ontext: (decodedText: string) => {
160-
if (disallowedTags.includes(currentTag())) return;
161-
output.push(decodedText);
162-
},
163-
164-
onclosetag: (name: string) => {
165-
if (currentTag() === name.toLowerCase()) tagStack.pop();
166-
},
167-
168-
}, { decodeEntities: true });
169-
170-
parser.write(html);
171-
parser.end();
172-
173-
return output.join('').replace(/\s+/g, ' ');
174-
}
175140
}
176141

177142
export default new HtmlUtils();

packages/renderer/htmlUtils.test.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import htmlUtils from './htmlUtils';
2+
3+
describe('htmlUtils', () => {
4+
5+
test('should strip off HTML', () => {
6+
const testCases = [
7+
[
8+
'',
9+
'',
10+
],
11+
[
12+
'<b>test</b>',
13+
'test',
14+
],
15+
[
16+
'Joplin&circledR;',
17+
'Joplin®',
18+
],
19+
[
20+
'&lt;b&gttest&lt;/b&gt',
21+
'&lt;b>test&lt;/b>',
22+
],
23+
];
24+
25+
for (const t of testCases) {
26+
const [input, expected] = t;
27+
const actual = htmlUtils.stripHtml(input);
28+
expect(actual).toBe(expected);
29+
}
30+
});
31+
32+
});

packages/renderer/htmlUtils.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,7 @@ class HtmlUtils {
9797
return selfClosingElements.includes(tagName.toLowerCase());
9898
}
9999

100-
// TODO: copied from @joplin/lib
101-
stripHtml(html: string) {
100+
public stripHtml(html: string) {
102101
const output: string[] = [];
103102

104103
const tagStack: string[] = [];
@@ -130,7 +129,14 @@ class HtmlUtils {
130129
parser.write(html);
131130
parser.end();
132131

133-
return output.join('').replace(/\s+/g, ' ');
132+
// In general, we want to get back plain text from this function, so all
133+
// HTML entities are decoded. Howver, to prevent XSS attacks, we
134+
// re-encode all the "<" characters, which should break any attempt to
135+
// inject HTML tags.
136+
137+
return output.join('')
138+
.replace(/\s+/g, ' ')
139+
.replace(/</g, '&lt;');
134140
}
135141

136142
public sanitizeHtml(html: string, options: any = null) {

0 commit comments

Comments
 (0)