From f8e56d8f7ed217e1e4a333231b2d627277119ddd Mon Sep 17 00:00:00 2001 From: mrholek Date: Sun, 5 Jul 2026 23:00:13 +0200 Subject: [PATCH] fix(pickers,RangeSlider,Autocomplete): route config strings through the sanitizer Several developer-config strings were written to innerHTML while skipping the component's own sanitizer/escaping: - RangeSlider tooltip update path (init path already sanitized) - Autocomplete searchNoResultsLabel (multi-select already used textContent) - TimePicker / DateRangePicker footer buttons and DateRangePicker ranges keys - Calendar weekNumbersLabel and navigation aria-label attributes Sanitize (RangeSlider) or set via textContent / HTML-escape (others) so config can no longer inject markup. --- js/src/autocomplete.js | 2 +- js/src/calendar.js | 12 ++++++------ js/src/date-range-picker.js | 8 ++++---- js/src/range-slider.js | 4 +++- js/src/time-picker.js | 4 ++-- js/tests/unit/autocomplete.spec.js | 17 +++++++++++++++++ js/tests/unit/calendar.spec.js | 25 +++++++++++++++++++++++++ js/tests/unit/range-slider.spec.js | 15 +++++++++++++++ 8 files changed, 73 insertions(+), 14 deletions(-) diff --git a/js/src/autocomplete.js b/js/src/autocomplete.js index 3a7af7b2e..413412c15 100644 --- a/js/src/autocomplete.js +++ b/js/src/autocomplete.js @@ -926,7 +926,7 @@ class Autocomplete extends BaseComponent { if (this._config.searchNoResultsLabel) { const placeholder = document.createElement('div') placeholder.classList.add(CLASS_NAME_OPTIONS_EMPTY) - placeholder.innerHTML = this._config.searchNoResultsLabel + placeholder.textContent = this._config.searchNoResultsLabel if (!SelectorEngine.findOne(SELECTOR_OPTIONS_EMPTY, this._menu)) { SelectorEngine.findOne(SELECTOR_OPTIONS, this._menu).append(placeholder) diff --git a/js/src/calendar.js b/js/src/calendar.js index 3c2ee0bb7..36dfec862 100644 --- a/js/src/calendar.js +++ b/js/src/calendar.js @@ -10,7 +10,7 @@ import BaseComponent from './base-component.js' import EventHandler from './dom/event-handler.js' import Manipulator from './dom/manipulator.js' import SelectorEngine from './dom/selector-engine.js' -import { DefaultAllowlist, sanitizeHtml } from './util/sanitizer.js' +import { DefaultAllowlist, escapeHtml, sanitizeHtml } from './util/sanitizer.js' import { defineJQueryPlugin } from './util/index.js' import { convertToDateObject, @@ -606,10 +606,10 @@ class Calendar extends BaseComponent { navigationElement.classList.add('calendar-nav') navigationElement.innerHTML = `
- - ${this._view === 'days' ? `` : ''}
@@ -622,10 +622,10 @@ class Calendar extends BaseComponent {
- ${this._view === 'days' ? `` : ''} -
@@ -644,7 +644,7 @@ class Calendar extends BaseComponent { ${this._config.showWeekNumber ? `
- ${this._config.weekNumbersLabel ?? ''} + ${this._config.weekNumbersLabel ? escapeHtml(this._config.weekNumbersLabel) : ''}
` : '' } diff --git a/js/src/date-range-picker.js b/js/src/date-range-picker.js index 97e90379a..c7f6fbbd9 100644 --- a/js/src/date-range-picker.js +++ b/js/src/date-range-picker.js @@ -779,7 +779,7 @@ class DateRangePicker extends BaseComponent { this._calendar.update(this._getCalendarConfig()) }) - buttonEl.innerHTML = key + buttonEl.textContent = key dateRangePickerRangesEl.append(buttonEl) } @@ -881,7 +881,7 @@ class DateRangePicker extends BaseComponent { const todayButtonEl = document.createElement('button') todayButtonEl.classList.add(...this._getButtonClasses(this._config.todayButtonClasses)) todayButtonEl.type = 'button' - todayButtonEl.innerHTML = this._config.todayButton + todayButtonEl.textContent = this._config.todayButton todayButtonEl.addEventListener('click', () => { const date = new Date() this._calendarDate = date @@ -901,7 +901,7 @@ class DateRangePicker extends BaseComponent { const cancelButtonEl = document.createElement('button') cancelButtonEl.classList.add(...this._getButtonClasses(this._config.cancelButtonClasses)) cancelButtonEl.type = 'button' - cancelButtonEl.innerHTML = this._config.cancelButton + cancelButtonEl.textContent = this._config.cancelButton cancelButtonEl.addEventListener('click', () => { this.cancel() }) @@ -913,7 +913,7 @@ class DateRangePicker extends BaseComponent { const confirmButtonEl = document.createElement('button') confirmButtonEl.classList.add(...this._getButtonClasses(this._config.confirmButtonClasses)) confirmButtonEl.type = 'button' - confirmButtonEl.innerHTML = this._config.confirmButton + confirmButtonEl.textContent = this._config.confirmButton confirmButtonEl.addEventListener('click', () => { this.hide() }) diff --git a/js/src/range-slider.js b/js/src/range-slider.js index e80279b21..f4847ec38 100644 --- a/js/src/range-slider.js +++ b/js/src/range-slider.js @@ -406,7 +406,9 @@ class RangeSlider extends BaseComponent { } if (this._tooltips[index]) { - this._tooltips[index].children[0].innerHTML = this._config.tooltipsFormat ? this._config.tooltipsFormat(value) : value + this._tooltips[index].children[0].innerHTML = this._config.tooltipsFormat ? + (this._config.sanitize ? sanitizeHtml(this._config.tooltipsFormat(value), this._config.allowList, this._config.sanitizeFn) : this._config.tooltipsFormat(value)) : + value const input = SelectorEngine.find(SELECTOR_RANGE_SLIDER_INPUT, this._element)[index] this._positionTooltip(this._tooltips[index], input) } diff --git a/js/src/time-picker.js b/js/src/time-picker.js index aecd98d5a..403bbf311 100644 --- a/js/src/time-picker.js +++ b/js/src/time-picker.js @@ -773,7 +773,7 @@ class TimePicker extends BaseComponent { ...this._getButtonClasses(this._config.cancelButtonClasses) ) cancelButtonEl.type = 'button' - cancelButtonEl.innerHTML = this._config.cancelButton + cancelButtonEl.textContent = this._config.cancelButton cancelButtonEl.addEventListener('click', () => { this.cancel() }) @@ -787,7 +787,7 @@ class TimePicker extends BaseComponent { ...this._getButtonClasses(this._config.confirmButtonClasses) ) confirmButtonEl.type = 'button' - confirmButtonEl.innerHTML = this._config.confirmButton + confirmButtonEl.textContent = this._config.confirmButton confirmButtonEl.addEventListener('click', () => { this.hide() }) diff --git a/js/tests/unit/autocomplete.spec.js b/js/tests/unit/autocomplete.spec.js index e90490b9c..af9e44a28 100644 --- a/js/tests/unit/autocomplete.spec.js +++ b/js/tests/unit/autocomplete.spec.js @@ -479,6 +479,23 @@ describe('Autocomplete', () => { expect(autocomplete._isShown()).toBe(true) }) + it('should render searchNoResultsLabel as text, not markup', () => { + fixtureEl.innerHTML = '
' + const autocompleteEl = fixtureEl.querySelector('.autocomplete') + const autocomplete = new Autocomplete(autocompleteEl, { + searchNoResultsLabel: '', + options: [{ label: 'Option 1', value: '1' }] + }) + + autocomplete.show() + autocomplete._search = 'nonexistent' + autocomplete._filterOptionsList() + + const placeholder = autocomplete._menu.querySelector('.autocomplete-options-empty') + expect(placeholder.querySelector('img')).toBeNull() + expect(placeholder.textContent).toBe('') + }) + it('should show with container mode', () => { fixtureEl.innerHTML = '
' const autocompleteEl = fixtureEl.querySelector('.autocomplete') diff --git a/js/tests/unit/calendar.spec.js b/js/tests/unit/calendar.spec.js index 4adb7ccad..5e531e864 100644 --- a/js/tests/unit/calendar.spec.js +++ b/js/tests/unit/calendar.spec.js @@ -2664,5 +2664,30 @@ describe('Calendar', () => { expect(btnDoubleNext.getAttribute('aria-label')).toEqual('Go to next year') expect(btnDoublePrev.getAttribute('aria-label')).toEqual('Go to previous year') }) + + it('should escape aria labels so they cannot break out of the attribute', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + new Calendar(div, { // eslint-disable-line no-new + ariaNavNextYearLabel: '">' + }) + + expect(div.querySelector('.calendar-nav img')).toBeNull() + expect(div.querySelector('.btn-double-next').getAttribute('aria-label')) + .toEqual('">') + }) + + it('should escape the week numbers label', () => { + fixtureEl.innerHTML = '
' + + const div = fixtureEl.querySelector('div') + new Calendar(div, { // eslint-disable-line no-new + showWeekNumber: true, + weekNumbersLabel: '' + }) + + expect(div.querySelector('.calendar-header-cell-inner img')).toBeNull() + }) }) }) diff --git a/js/tests/unit/range-slider.spec.js b/js/tests/unit/range-slider.spec.js index 9a7904743..80e0a45c7 100644 --- a/js/tests/unit/range-slider.spec.js +++ b/js/tests/unit/range-slider.spec.js @@ -113,6 +113,21 @@ describe('RangeSlider', () => { expect(tooltips[1].textContent).toBe('150%') }) + it('should sanitize tooltip content on the update path', () => { + fixtureEl.innerHTML = '
' + const element = fixtureEl.querySelector('#slider') + const rangeSlider = new RangeSlider(element, { + tooltips: true, + value: 50, + tooltipsFormat: () => '' + }) + + rangeSlider._updateTooltip(0, 60) + + const inner = element.querySelector('.range-slider-tooltip-inner') + expect(inner.querySelector('img').hasAttribute('onerror')).toBeFalse() + }) + it('should initialize with a single numeric value', () => { fixtureEl.innerHTML = '
' const element = fixtureEl.querySelector('#slider')