Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ jobs:
echo "::remove-matcher owner=eslint-stylish::"
- run: npm run all
- run: npm run test

semgrep:
name: semgrep-oss/scan
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
if: (github.actor != 'dependabot[bot]')
steps:
- uses: actions/checkout@v4
- run: semgrep scan --error --config .semgrep/
31 changes: 31 additions & 0 deletions .semgrep/xss-prevention.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
rules:
- id: d3-unsanitized-html
languages: [javascript, typescript]
message: |
Potential XSS: .html() called with unsanitized content.
Use utilSanitizeHTML() for external/user data, .text() for plain text,
or l10n.t()/l10n.tHtml() for trusted localization strings.
If this is a false positive, add: // nosemgrep: d3-unsanitized-html
severity: WARNING
patterns:
- pattern: $SEL.html($CONTENT)
# Exclude safe patterns - sanitized content:
- pattern-not: $SEL.html(utilSanitizeHTML(...))
- pattern-not: $SEL.html($X => utilSanitizeHTML(...))
- pattern-not: $SEL.html(function($X) { return utilSanitizeHTML(...); })
# Exclude safe patterns - localization (trusted developer content):
- pattern-not: $SEL.html(l10n.tHtml(...))
- pattern-not: $SEL.html(l10n.t(...))
- pattern-not: $SEL.html(uifield.tHtml(...))
# Exclude safe patterns - marked.parse (typically on trusted content):
- pattern-not: $SEL.html(marked.parse(...))
# Exclude clearing/resetting content:
- pattern-not: $SEL.html('')
- pattern-not: $SEL.html("")
- pattern-not: $SEL.html(' ')
- pattern-not: $SEL.html(" ")
# Exclude reading innerHTML (no arguments):
- pattern-not: $SEL.html()
paths:
include:
- modules/
1 change: 1 addition & 0 deletions modules/ui/UiField.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export class UiField {
.append('span')
.attr('class', 'label-text');

// nosemgrep: d3-unsanitized-html - preset field labels are trusted
textEnter
.append('span')
.attr('class', 'label-textvalue')
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/UiRapidCatalog.js
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ export class UiRapidCatalog extends EventEmitter {
.classed('added', d => d.added)
.classed('hide', d => d.filtered);

// nosemgrep: d3-unsanitized-html - highlight() sanitizes internally
$datasets.selectAll('.rapid-catalog-dataset-name')
.html(d => this.highlight(this._filterText, d.getLabel()));

Expand All @@ -488,6 +489,7 @@ export class UiRapidCatalog extends EventEmitter {
$datasets.selectAll('.dataset-category-preview')
.attr('title', l10n.t('rapid_poweruser.beta')); // alt text

// nosemgrep: d3-unsanitized-html - highlight() sanitizes internally
$datasets.selectAll('.rapid-catalog-dataset-snippet')
.html(d => this.highlight(this._filterText, d.getDescription()));

Expand Down
1 change: 1 addition & 0 deletions modules/ui/combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,7 @@ export function uiCombobox(context, klass) {
if (typeof d.display === 'function') { // display function
selection.call(d.display);
} else if (d.display) { // display html value
// nosemgrep: d3-unsanitized-html - display values from internal presets
selection.html(d.display);
} else { // text value
selection.text(d.value);
Expand Down
1 change: 1 addition & 0 deletions modules/ui/commit_warnings.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function uiCommitWarnings(context) {
.append('div')
.attr('class', 'modal-section ' + section + ' fillL2');

// nosemgrep: d3-unsanitized-html - ternary with l10n.tHtml
containerEnter
.append('h3')
.html(severity === 'warning' ? l10n.tHtml('commit.warnings') : l10n.tHtml('commit.errors'));
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/conflicts.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ export function uiConflicts(context) {
.append('div')
.attr('class', 'conflict-detail-container');

// nosemgrep: d3-unsanitized-html - uses utilSanitizeHTML
details
.append('ul')
.attr('class', 'conflict-detail-list')
Expand All @@ -190,6 +191,7 @@ export function uiConflicts(context) {
.attr('class', 'conflict-choices')
.call(addChoices);

// nosemgrep: d3-unsanitized-html - l10n.tHtml returns trusted l10n content
details
.append('div')
.attr('class', 'conflict-nav-buttons joined cf')
Expand Down
1 change: 1 addition & 0 deletions modules/ui/fields/access.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function uiFieldAccess(context, uifield) {
.append('li')
.attr('class', d => `labeled-input preset-access-${d}`);

// nosemgrep: d3-unsanitized-html - uifield.tHtml returns trusted l10n content
enter
.append('div')
.attr('class', 'label preset-label-access')
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/fields/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export function uiFieldCheck(context, uifield) {
.attr('type', 'checkbox')
.attr('id', uifield.uid);

// nosemgrep: d3-unsanitized-html - texts from internal field options
enter
.append('span')
.html(texts[0])
Expand Down Expand Up @@ -212,6 +213,7 @@ export function uiFieldCheck(context, uifield) {
.property('indeterminate', isMixed || (uifield.type !== 'defaultCheck' && !_value))
.property('checked', isChecked(_value));

// nosemgrep: d3-unsanitized-html - textFor returns l10n content
text
.html(isMixed ? l10n.tHtml('inspector.multiple_values') : textFor(_value))
.classed('mixed', isMixed);
Expand Down
1 change: 1 addition & 0 deletions modules/ui/fields/cycleway.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export function uiFieldCycleway(context, uifield) {
.append('li')
.attr('class', function(d) { return 'labeled-input preset-cycleway-' + stripcolon(d); });

// nosemgrep: d3-unsanitized-html - uifield.tHtml returns trusted l10n content
enter
.append('div')
.attr('class', 'label preset-label-cycleway')
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/fields/radio.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export function uiFieldRadio(context, uifield) {
.attr('value', function(d) { return uifield.t(`options.${d}`, { 'default': d }); })
.attr('checked', false);

// nosemgrep: d3-unsanitized-html - uifield.tHtml returns trusted l10n content
enter
.append('span')
.html(function(d) { return uifield.tHtml(`options.${d}`, { 'default': d }); });
Expand Down Expand Up @@ -299,6 +300,7 @@ export function uiFieldRadio(context, uifield) {
if (selection.empty()) {
placeholder.html(l10n.tHtml('inspector.none'));
} else {
// nosemgrep: d3-unsanitized-html - value from radio button set by l10n
placeholder.html(selection.attr('value'));
_oldType[selection.datum()] = tags[selection.datum()];
}
Expand Down
1 change: 1 addition & 0 deletions modules/ui/fields/wikidata.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export function uiFieldWikidata(context, uifield) {
.append('li')
.attr('class', function(d) { return 'labeled-input preset-wikidata-' + d; });

// nosemgrep: d3-unsanitized-html - l10n.tHtml returns trusted l10n content
enter
.append('div')
.attr('class', 'label')
Expand Down
1 change: 1 addition & 0 deletions modules/ui/flash.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function uiFlash(context) {
.selectAll('.flash-icon use')
.attr('xlink:href', _iconName);

// nosemgrep: d3-unsanitized-html - flash labels from internal UI code
content
.selectAll('.flash-text')
.attr('class', 'flash-text')
Expand Down
1 change: 1 addition & 0 deletions modules/ui/intro/UiCurtain.js
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ export class UiCurtain {
html += `<div class="button-section"><button href="#" class="button action">${opts.buttonText}</button></div>`;
}

// nosemgrep: d3-unsanitized-html - intro tutorial content from internal code
this.$tooltip
.attr('class', klass)
.selectAll('.popover-inner')
Expand Down
1 change: 1 addition & 0 deletions modules/ui/keepRight_details.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export function uiKeepRightDetails(context) {
.append('h4')
.text(l10n.t('QA.keepRight.detail_description'));

// nosemgrep: d3-unsanitized-html - uses utilSanitizeHTML
descriptionEnter
.append('div')
.attr('class', 'qa-details-description-text')
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/maproulette_details.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export function uiMapRouletteDetails(context) {
const descContent = descSection
.append('section')
.attr('class', 'qa-details-container');
// nosemgrep: d3-unsanitized-html - already sanitized above
descContent
.html(descriptionHtml)
.selectAll('a')
Expand All @@ -190,6 +191,7 @@ export function uiMapRouletteDetails(context) {
const instructionContent = instructionSection
.append('article')
.attr('class', 'qa-details-container');
// nosemgrep: d3-unsanitized-html - already sanitized above
instructionContent
.html(instructionHtml)
.selectAll('a')
Expand Down
1 change: 1 addition & 0 deletions modules/ui/maproulette_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ export function uiMapRouletteEditor(context) {
// update
commentSave = commentSaveEnter.merge(commentSave);

// nosemgrep: d3-unsanitized-html - l10n + internal action string
commentSave.select('.note-save-header') // Corrected class name
.html(l10n.t('map_data.layers.maproulette.comment') +
' <span style="color: ' + getActionColor(_actionTaken) + ';">' + _actionTaken + '</span>'
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/note_comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,13 @@ export function uiNoteComments(context) {
}
});

// nosemgrep: d3-unsanitized-html - l10n.t with trusted date formatting
metadataEnter
.append('div')
.attr('class', 'comment-date')
.html(d => l10n.t(`note.status.${d.action}`, { when: localeDateString(d.date) }));

// NOTE: d.html comes from OSM API and must be sanitized
mainEnter
.append('div')
.attr('class', 'comment-text')
Expand Down
5 changes: 5 additions & 0 deletions modules/ui/osmose_details.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function uiOsmoseDetails(context) {
.append('h4')
.text(l10n.t('QA.keepRight.detail_description'));

// nosemgrep: d3-unsanitized-html - sanitized in OsmoseService
div
.append('p')
.attr('class', 'qa-details-description-text')
Expand Down Expand Up @@ -75,6 +76,7 @@ export function uiOsmoseDetails(context) {
.append('h4')
.text(l10n.t('QA.osmose.fix_title'));

// nosemgrep: d3-unsanitized-html - sanitized in OsmoseService
div
.append('p')
.html(d => issueString(d, 'fix'))
Expand All @@ -93,6 +95,7 @@ export function uiOsmoseDetails(context) {
.append('h4')
.text(l10n.t('QA.osmose.trap_title'));

// nosemgrep: d3-unsanitized-html - sanitized in OsmoseService
div
.append('p')
.html(d => issueString(d, 'trap'))
Expand All @@ -117,6 +120,7 @@ export function uiOsmoseDetails(context) {
.append('h4')
.text(l10n.t('QA.osmose.detail_title'));

// nosemgrep: d3-unsanitized-html - d.detail already sanitized in OsmoseService
detailsDiv
.append('p')
.html(d => d.detail)
Expand All @@ -130,6 +134,7 @@ export function uiOsmoseDetails(context) {
.append('h4')
.text(l10n.t('QA.osmose.elems_title'));

// nosemgrep: d3-unsanitized-html - element IDs from OSM data
elemsDiv
.append('ul').selectAll('li')
.data(d.elems)
Expand Down
1 change: 1 addition & 0 deletions modules/ui/panes/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ export function uiPaneHelp(context) {
helpPane.selectAll('.pane-heading > h2').text(d.title);

const content = _selection.selectAll('.help-content');
// nosemgrep: d3-unsanitized-html - help content from internal docs
content.html(d.contentHtml);
content.selectAll('a').attr('target', '_blank'); // outbound links should open in new tab

Expand Down
2 changes: 2 additions & 0 deletions modules/ui/preset_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ export function uiPresetList(context) {
.append('div')
.attr('class', 'label-inner');

// nosemgrep: d3-unsanitized-html - preset names are trusted
labelEnter
.append('div')
.attr('class', 'namepart')
Expand Down Expand Up @@ -476,6 +477,7 @@ export function uiPresetList(context) {
preset.subtitleLabel()
].filter(Boolean);

// nosemgrep: d3-unsanitized-html - preset name parts are trusted
labelEnter.selectAll('.namepart')
.data(nameparts)
.enter()
Expand Down
1 change: 1 addition & 0 deletions modules/ui/sections/feature_type.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export function uiSectionFeatureType(context) {
nameparts.exit()
.remove();

// nosemgrep: d3-unsanitized-html - preset names from internal system
nameparts
.enter()
.append('div')
Expand Down
1 change: 1 addition & 0 deletions modules/ui/sections/raw_member_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ export function uiSectionRawMemberEditor(context) {
.attr('href', '#')
.on('click', selectMember);

// nosemgrep: d3-unsanitized-html - preset names are trusted
labelLink
.append('span')
.attr('class', 'member-entity-type')
Expand Down
1 change: 1 addition & 0 deletions modules/ui/sections/raw_membership_editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ export function uiSectionRawMembershipEditor(context) {
.attr('href', '#')
.on('click', selectRelation);

// nosemgrep: d3-unsanitized-html - preset names are trusted
labelLink
.append('span')
.attr('class', 'member-entity-type')
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/sections/selection_list.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,11 @@ export function uiSectionSelectionList(context) {
return '#rapid-icon-' + entity.geometry(graph);
});

// nosemgrep: d3-unsanitized-html - preset names are trusted
items.selectAll('.entity-type')
.html(entity => presets.match(entity, graph).name());

// nosemgrep: d3-unsanitized-html - l10n.displayName returns trusted content
items.selectAll('.entity-name')
.html(d => {
const entity = graph.entity(d.id);
Expand Down
1 change: 1 addition & 0 deletions modules/ui/sections/validation_rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export function uiSectionValidationRules(context) {
.attr('name', 'rule')
.on('change', toggleRule);

// nosemgrep: d3-unsanitized-html - l10n.tHtml with internal params
label
.append('span')
.html(d => {
Expand Down
1 change: 1 addition & 0 deletions modules/ui/settings/custom_background.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ ${info}

let textSection = modal.select('.modal-section.message-text');

// nosemgrep: d3-unsanitized-html - instructions built from l10n strings
textSection
.append('div')
.attr('class', 'instructions-template')
Expand Down
2 changes: 2 additions & 0 deletions modules/ui/settings/custom_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ ${file_instructions}
${file_tip}
`);

// nosemgrep: d3-unsanitized-html - fileHtml built from l10n strings
textSection
.append('div')
.attr('class', 'instructions-template')
Expand Down Expand Up @@ -106,6 +107,7 @@ ${url_tokens}
* \`${url_example_pmtiles}\`
`);

// nosemgrep: d3-unsanitized-html - urlHtml built from l10n strings
textSection
.append('div')
.attr('class', 'instructions-template')
Expand Down
1 change: 1 addition & 0 deletions modules/ui/tag_reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function uiTagReference(context, what) {
docsHtml = l10n.tHtml('inspector.no_documentation_key');
}

// nosemgrep: d3-unsanitized-html - docsHtml from l10n.htmlForLocalizedText or l10n.tHtml
_body
.append('p')
.attr('class', 'tag-reference-description')
Expand Down
3 changes: 2 additions & 1 deletion modules/ui/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ export function uiTooltip(context) {
textWrap.exit()
.remove();

// nosemgrep: d3-unsanitized-html - tooltip content from internal UI code
textWrap.enter()
.append('div')
.attr('class', 'tooltip-text')
.merge(textWrap)
.html(d => d); // watch out: a few tooltips still send html through here
.html(d => d);

const shortcutWrap = selection
.selectAll('.tooltip-keyhint')
Expand Down