Skip to content

Commit 4aa3c86

Browse files
Use DOMPurify to sanitize all possible HTML render paths
More fixes
1 parent d425464 commit 4aa3c86

14 files changed

Lines changed: 88 additions & 31 deletions

modules/services/EsriService.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { utilQsString } from '@rapid-sdk/util';
55
import { AbstractSystem } from '../core/AbstractSystem.js';
66
import { Graph, Tree, RapidDataset } from '../core/lib/index.js';
77
import { osmNode, osmRelation, osmWay } from '../osm/index.js';
8-
import { utilFetchResponse } from '../util/index.js';
8+
import { utilFetchResponse, utilSanitizeHTML } from '../util/index.js';
99

1010

1111
const GROUPID = 'bdf6c800b3ae453b9db239e03d7c1727';
@@ -281,13 +281,13 @@ export class EsriService extends AbstractSystem {
281281
ds.lastv = null;
282282
ds.layer = null; // the schema info will live here
283283

284-
// Cleanup the `licenseInfo` field by removing styles (not used currently)
284+
// Cleanup and sanitize the `licenseInfo` field from Esri API
285285
const license = select(document.createElement('div'));
286-
license.html(ds.licenseInfo); // set innerHtml
286+
license.html(utilSanitizeHTML(ds.licenseInfo)); // sanitize then set innerHTML
287287
license.selectAll('*')
288288
.attr('style', null)
289289
.attr('size', null);
290-
ds.license_html = license.html(); // get innerHtml
290+
ds.license_html = license.html(); // get innerHTML
291291
}
292292

293293

modules/services/KeepRightService.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import RBush from 'rbush';
44

55
import { AbstractSystem } from '../core/AbstractSystem.js';
66
import { QAItem } from '../osm/qa_item.js';
7-
import { utilFetchResponse } from '../util/index.js';
7+
import { utilFetchResponse, utilSanitizeHTML } from '../util/index.js';
88

99

1010
const KEEPRIGHT_API = 'https://www.keepright.at';
@@ -470,8 +470,8 @@ export class KeepRightService extends AbstractSystem {
470470
idType = 'IDs' in issueTemplate ? issueTemplate.IDs[i-1] : '';
471471
if (idType && capture) { // link IDs if present in the capture
472472
capture = this._parseError(capture, idType);
473-
} else if (htmlRegex.test(capture)) { // escape any html in non-IDs
474-
capture = '\\' + capture + '\\';
473+
} else if (htmlRegex.test(capture)) { // sanitize any html in non-IDs
474+
capture = utilSanitizeHTML(capture);
475475
} else {
476476
const compare = capture.toLowerCase();
477477
if (this._krData.localizeStrings[compare]) { // some replacement strings can be localized

modules/services/OsmoseService.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import RBush from 'rbush';
66

77
import { AbstractSystem } from '../core/AbstractSystem.js';
88
import { QAItem } from '../osm/qa_item.js';
9-
import { utilFetchResponse } from '../util/index.js';
9+
import { utilFetchResponse, utilSanitizeHTML } from '../util/index.js';
1010

1111

1212
const TILEZOOM = 14;
@@ -198,7 +198,7 @@ export class OsmoseService extends AbstractSystem {
198198
// Assign directly for immediate use in the callback
199199
issue.elems = data.elems.map(e => e.type.substring(0,1) + e.id);
200200
// Some issues have instance specific detail in a subtitle
201-
issue.detail = data.subtitle ? marked.parse(data.subtitle.auto) : '';
201+
issue.detail = data.subtitle ? utilSanitizeHTML(marked.parse(data.subtitle.auto)) : '';
202202
this.replaceItem(issue);
203203
};
204204

@@ -433,9 +433,9 @@ export class OsmoseService extends AbstractSystem {
433433
let issueStrings = {};
434434
// Force title to begin with an uppercase letter
435435
if (title) issueStrings.title = title.auto.charAt(0).toUpperCase() + title.auto.slice(1);
436-
if (detail) issueStrings.detail = marked.parse(detail.auto);
437-
if (trap) issueStrings.trap = marked.parse(trap.auto);
438-
if (fix) issueStrings.fix = marked.parse(fix.auto);
436+
if (detail) issueStrings.detail = utilSanitizeHTML(marked.parse(detail.auto));
437+
if (trap) issueStrings.trap = utilSanitizeHTML(marked.parse(trap.auto));
438+
if (fix) issueStrings.fix = utilSanitizeHTML(marked.parse(fix.auto));
439439

440440
stringData[itemType] = issueStrings;
441441
};

modules/ui/UiAttribution.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { selection, select } from 'd3-selection';
22
import throttle from 'lodash-es/throttle.js';
33

4+
import { utilSanitizeHTML } from '../util/index.js';
5+
46

57
/**
68
* UiAttribution
@@ -113,9 +115,9 @@ export class UiAttribution {
113115
.each((d, i, nodes) => {
114116
const $$link = select(nodes[i]);
115117

116-
// add html directly (maybe we shouldn't?)
118+
// Sanitize HTML from imagery provider metadata
117119
if (d.terms_html) {
118-
$$link.html(d.terms_html);
120+
$$link.html(utilSanitizeHTML(d.terms_html));
119121
return;
120122
}
121123

modules/ui/UiRapidCatalog.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { marked } from 'marked';
44

55
import { uiIcon } from './icon.js';
66
import { uiCombobox} from './combobox.js';
7-
import { utilKeybinding, utilNoAuto } from '../util/index.js';
7+
import { utilKeybinding, utilNoAuto, utilSanitizeHTML } from '../util/index.js';
88

99
const MAXRESULTS = 100;
1010

@@ -563,9 +563,12 @@ export class UiRapidCatalog extends EventEmitter {
563563

564564
/**
565565
* highlight
566+
* Highlights matches of `needle` in `haystack` with <mark> tags.
567+
* Sanitizes the haystack first to prevent XSS.
566568
*/
567569
highlight(needle, haystack) {
568-
let html = haystack;
570+
// Sanitize the haystack to prevent XSS from dataset labels
571+
let html = utilSanitizeHTML(haystack);
569572
if (needle) {
570573
const re = new RegExp('\(' + _escapeRegex(needle) + '\)', 'gi');
571574
html = html.replace(re, '<mark>$1</mark>');

modules/ui/conflicts.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Extent, numWrap } from '@rapid-sdk/math';
55
import { JXON } from '../util/jxon.js';
66
import { osmChangeset } from '../osm/index.js';
77
import { uiIcon } from './icon.js';
8-
import { utilDetect, utilKeybinding, utilRebind } from '../util/index.js';
8+
import { utilDetect, utilKeybinding, utilRebind, utilSanitizeHTML } from '../util/index.js';
99

1010

1111
export function uiConflicts(context) {
@@ -183,7 +183,7 @@ export function uiConflicts(context) {
183183
.enter()
184184
.append('li')
185185
.attr('class', 'conflict-detail-item')
186-
.html(d => d);
186+
.html(d => utilSanitizeHTML(d));
187187

188188
details
189189
.append('div')

modules/ui/keepRight_details.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { select as d3_select } from 'd3-selection';
22

3-
import { utilHighlightEntities } from '../util/index.js';
3+
import { utilHighlightEntities, utilSanitizeHTML } from '../util/index.js';
44

55

66
export function uiKeepRightDetails(context) {
@@ -52,7 +52,7 @@ export function uiKeepRightDetails(context) {
5252
descriptionEnter
5353
.append('div')
5454
.attr('class', 'qa-details-description-text')
55-
.html(issueDetailHTML);
55+
.html(d => utilSanitizeHTML(issueDetailHTML(d)));
5656

5757
// If there are entity links in the error message..
5858
let relatedEntities = [];

modules/ui/maproulette_details.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { select as d3_select } from 'd3-selection';
22

3-
import { utilHighlightEntities } from '../util/index.js';
3+
import { utilHighlightEntities, utilSanitizeHTML } from '../util/index.js';
44
import { marked } from 'marked';
55

66
export function uiMapRouletteDetails(context) {
@@ -153,8 +153,8 @@ export function uiMapRouletteDetails(context) {
153153
.attr('target', '_blank');
154154
}
155155

156-
const descriptionHtml = generateDynamicContent(marked.parse(replaceMustacheTags(task.description, task), { async: false }));
157-
const instructionHtml = generateDynamicContent(marked.parse(replaceMustacheTags(task.instruction, task), { async: false }));
156+
const descriptionHtml = utilSanitizeHTML(generateDynamicContent(marked.parse(replaceMustacheTags(task.description, task), { async: false })));
157+
const instructionHtml = utilSanitizeHTML(generateDynamicContent(marked.parse(replaceMustacheTags(task.instruction, task), { async: false })));
158158

159159
// We show the challenge description when user select an unkown challenge.
160160
// But we hide it if a specific (assumed to be know) challenge is selected.

modules/ui/note_comments.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { select as d3_select } from 'd3-selection';
22

33
import { uiIcon } from './icon.js';
4+
import { utilSanitizeHTML } from '../util/index.js';
45

56

67
export function uiNoteComments(context) {
@@ -51,8 +52,11 @@ export function uiNoteComments(context) {
5152
.attr('href', osm.userURL(d.user))
5253
.attr('target', '_blank');
5354
}
54-
selection
55-
.html(d => d.user || l10n.tHtml('note.anonymous'));
55+
if (d.user) {
56+
selection.text(d.user);
57+
} else {
58+
selection.html(l10n.tHtml('note.anonymous'));
59+
}
5660
});
5761

5862
metadataEnter
@@ -63,7 +67,7 @@ export function uiNoteComments(context) {
6367
mainEnter
6468
.append('div')
6569
.attr('class', 'comment-text')
66-
.html(d => d.html)
70+
.html(d => utilSanitizeHTML(d.html))
6771
.selectAll('a')
6872
.attr('rel', 'noopener nofollow')
6973
.attr('target', '_blank');

modules/ui/success.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { resolveStrings } from 'osm-community-index';
55
import { uiIcon } from './icon.js';
66
import { uiDisclosure } from '../ui/disclosure.js';
77
import { utilRebind } from '../util/rebind.js';
8+
import { utilSanitizeHTML } from '../util/index.js';
89

910

1011
let _oci = null;
@@ -248,12 +249,12 @@ export function uiSuccess(context) {
248249
selection
249250
.append('div')
250251
.attr('class', 'community-name')
251-
.html(d.resolved.nameHTML);
252+
.html(utilSanitizeHTML(d.resolved.nameHTML));
252253

253254
selection
254255
.append('div')
255256
.attr('class', 'community-description')
256-
.html(d.resolved.descriptionHTML);
257+
.html(utilSanitizeHTML(d.resolved.descriptionHTML));
257258

258259
// Create an expanding section if any of these are present..
259260
if (d.resolved.extendedDescriptionHTML || (d.languageCodes && d.languageCodes.length)) {
@@ -310,7 +311,7 @@ export function uiSuccess(context) {
310311
moreEnter
311312
.append('div')
312313
.attr('class', 'community-extended-description')
313-
.html(d.resolved.extendedDescriptionHTML);
314+
.html(utilSanitizeHTML(d.resolved.extendedDescriptionHTML));
314315
}
315316

316317
if (d.languageCodes && d.languageCodes.length) {

0 commit comments

Comments
 (0)