diff --git a/HISTORY.md b/HISTORY.md index 5c8dcaaac..9a8f3dea0 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -5,6 +5,7 @@ https://github.com/josdejong/jsoneditor ## not yet published, version 9.0.2 +- Fix #1029: XSS vulnerabilities. - Fix #1017: unable to style the color of a value containing a color. Thanks @p3x-robot. diff --git a/src/js/ContextMenu.js b/src/js/ContextMenu.js index 9bcdb1ce7..30478a7c4 100644 --- a/src/js/ContextMenu.js +++ b/src/js/ContextMenu.js @@ -107,7 +107,9 @@ export class ContextMenu { buttonExpand.type = 'button' domItem.buttonExpand = buttonExpand buttonExpand.className = 'jsoneditor-expand' - buttonExpand.innerHTML = '
' + const buttonExpandInner = document.createElement('div') + buttonExpandInner.className = 'jsoneditor-expand' + buttonExpand.appendChild(buttonExpandInner) li.appendChild(buttonExpand) if (item.submenuTitle) { buttonExpand.title = item.submenuTitle @@ -141,8 +143,14 @@ export class ContextMenu { createMenuItems(ul, domSubItems, item.submenu) } else { // no submenu, just a button with clickhandler - button.innerHTML = '
' + - '
' + translate(item.text) + '
' + const icon = document.createElement('div') + icon.className = 'jsoneditor-icon' + button.appendChild(icon) + + const text = document.createElement('div') + text.className = 'jsoneditor-text' + text.appendChild(document.createTextNode(translate(item.text))) + button.appendChild(text) } domItems.push(domItem) @@ -425,5 +433,3 @@ export class ContextMenu { // currently displayed context menu, a singleton. We may only have one visible context menu ContextMenu.visibleMenu = undefined - -export default ContextMenu diff --git a/src/js/ErrorTable.js b/src/js/ErrorTable.js index 0da876132..136005486 100644 --- a/src/js/ErrorTable.js +++ b/src/js/ErrorTable.js @@ -23,7 +23,7 @@ export class ErrorTable { const additionalErrorsIndication = document.createElement('div') additionalErrorsIndication.style.display = 'none' additionalErrorsIndication.className = 'jsoneditor-additional-errors fadein' - additionalErrorsIndication.innerHTML = 'Scroll for more ▿' + additionalErrorsIndication.textContent = 'Scroll for more \u25BF' this.dom.additionalErrorsIndication = additionalErrorsIndication validationErrorsContainer.appendChild(additionalErrorsIndication) @@ -76,19 +76,15 @@ export class ErrorTable { if (this.errorTableVisible && errors.length > 0) { const validationErrors = document.createElement('div') validationErrors.className = 'jsoneditor-validation-errors' - validationErrors.innerHTML = '
' - const tbody = validationErrors.getElementsByTagName('tbody')[0] - errors.forEach(error => { - let message - if (typeof error === 'string') { - message = '
' + error + '
' - } else { - message = - '' + (error.dataPath || '') + '' + - '
' + error.message + '
' - } + const table = document.createElement('table') + table.className = 'jsoneditor-text-errors' + validationErrors.appendChild(table) + + const tbody = document.createElement('tbody') + table.appendChild(tbody) + errors.forEach(error => { let line if (!isNaN(error.line)) { @@ -108,7 +104,36 @@ export class ErrorTable { trEl.className += ' validation-error' } - trEl.innerHTML = ('' + (!isNaN(line) ? ('Ln ' + line) : '') + '' + message) + const td1 = document.createElement('td') + const button = document.createElement('button') + button.className = 'jsoneditor-schema-error' + td1.appendChild(button) + trEl.appendChild(td1) + + const td2 = document.createElement('td') + td2.style = 'white-space: nowrap;' + td2.textContent = (!isNaN(line) ? ('Ln ' + line) : '') + trEl.appendChild(td2) + + if (typeof error === 'string') { + const td34 = document.createElement('td') + td34.colSpan = 2 + const pre = document.createElement('pre') + pre.appendChild(document.createTextNode(error)) + td34.appendChild(pre) + trEl.appendChild(td34) + } else { + const td3 = document.createElement('td') + td3.appendChild(document.createTextNode(error.dataPath || '')) + trEl.appendChild(td3) + + const td4 = document.createElement('td') + const pre = document.createElement('pre') + pre.appendChild(document.createTextNode(error.message)) + td4.appendChild(pre) + trEl.appendChild(td4) + } + trEl.onclick = () => { this.onFocusLine(line) } diff --git a/src/js/ModeSwitcher.js b/src/js/ModeSwitcher.js index cb9e00932..ec2cac3ac 100644 --- a/src/js/ModeSwitcher.js +++ b/src/js/ModeSwitcher.js @@ -83,7 +83,7 @@ export class ModeSwitcher { const box = document.createElement('button') box.type = 'button' box.className = 'jsoneditor-modes jsoneditor-separator' - box.innerHTML = currentTitle + ' ▾' + box.textContent = currentTitle + ' \u25BE' box.title = translate('modeEditorTitle') box.onclick = () => { const menu = new ContextMenu(items) diff --git a/src/js/Node.js b/src/js/Node.js index dad1eea09..fea028156 100644 --- a/src/js/Node.js +++ b/src/js/Node.js @@ -1511,7 +1511,7 @@ export class Node { if (this.valueInnerText === '' && this.dom.value.innerHTML !== '') { // When clearing the contents, often a
remains, messing up the // styling of the empty text box. Therefore we remove the
- this.dom.value.innerHTML = '' + this.dom.value.textContent = '' } } @@ -1718,14 +1718,14 @@ export class Node { // Create the default empty option this.dom.select.option = document.createElement('option') this.dom.select.option.value = '' - this.dom.select.option.innerHTML = '--' + this.dom.select.option.textContent = '--' this.dom.select.appendChild(this.dom.select.option) // Iterate all enum values and add them as options for (let i = 0; i < this.enum.length; i++) { this.dom.select.option = document.createElement('option') this.dom.select.option.value = this.enum[i] - this.dom.select.option.innerHTML = this.enum[i] + this.dom.select.option.textContent = this.enum[i] if (this.dom.select.option.value === this.value) { this.dom.select.option.selected = true } @@ -1747,7 +1747,7 @@ export class Node { ) { this.valueFieldHTML = this.dom.tdValue.innerHTML this.dom.tdValue.style.visibility = 'hidden' - this.dom.tdValue.innerHTML = '' + this.dom.tdValue.textContent = '' } else { delete this.valueFieldHTML } @@ -1804,7 +1804,7 @@ export class Node { }) } if (!title) { - this.dom.date.innerHTML = new Date(value).toISOString() + this.dom.date.textContent = new Date(value).toISOString() } else { while (this.dom.date.firstChild) { this.dom.date.removeChild(this.dom.date.firstChild) @@ -1892,7 +1892,7 @@ export class Node { if (this.fieldInnerText === '' && this.dom.field.innerHTML !== '') { // When clearing the contents, often a
remains, messing up the // styling of the empty text box. Therefore we remove the
- this.dom.field.innerHTML = '' + this.dom.field.textContent = '' } } @@ -2349,7 +2349,7 @@ export class Node { child.index = index const childField = child.dom.field if (childField) { - childField.innerHTML = index + childField.textContent = index } }) } else if (this.type === 'object') { @@ -2375,10 +2375,10 @@ export class Node { if (this.type === 'array') { domValue = document.createElement('div') - domValue.innerHTML = '[...]' + domValue.textContent = '[...]' } else if (this.type === 'object') { domValue = document.createElement('div') - domValue.innerHTML = '{...}' + domValue.textContent = '{...}' } else { if (!this.editable.value && isUrl(this.value)) { // create a link in case of read-only editor and value containing an url @@ -2525,14 +2525,14 @@ export class Node { // swap the value of a boolean when the checkbox displayed left is clicked if (type === 'change' && target === dom.checkbox) { - this.dom.value.innerHTML = !this.value + this.dom.value.textContent = String(!this.value) this._getDomValue() this._updateDomDefault() } // update the value of the node based on the selected option if (type === 'change' && target === dom.select) { - this.dom.value.innerHTML = dom.select.value + this.dom.value.innerHTML = this._escapeHTML(dom.select.value) this._getDomValue() this._updateDomValue() } @@ -4047,7 +4047,7 @@ export class Node { } } - this.dom.value.innerHTML = (this.type === 'object') + this.dom.value.textContent = (this.type === 'object') ? ('{' + (nodeName || count) + '}') : ('[' + (nodeName || count) + ']') } @@ -4555,7 +4555,7 @@ Node.onDuplicate = nodes => { if (clones[0].parent.type === 'object') { // when duplicating a single object property, // set focus to the field and keep the original field name - clones[0].dom.field.innerHTML = nodes[0].field + clones[0].dom.field.innerHTML = this._escapeHTML(nodes[0].field) clones[0].focus('field') } else { clones[0].focus() diff --git a/src/js/SearchBox.js b/src/js/SearchBox.js index ec7e418c5..b2c1b11bd 100644 --- a/src/js/SearchBox.js +++ b/src/js/SearchBox.js @@ -229,16 +229,16 @@ export class SearchBox { if (text !== undefined) { const resultCount = this.results.length if (resultCount === 0) { - this.dom.results.innerHTML = 'no results' + this.dom.results.textContent = 'no\u00A0results' } else if (resultCount === 1) { - this.dom.results.innerHTML = '1 result' + this.dom.results.textContent = '1\u00A0result' } else if (resultCount > MAX_SEARCH_RESULTS) { - this.dom.results.innerHTML = MAX_SEARCH_RESULTS + '+ results' + this.dom.results.textContent = MAX_SEARCH_RESULTS + '+\u00A0results' } else { - this.dom.results.innerHTML = resultCount + ' results' + this.dom.results.textContent = resultCount + '\u00A0results' } } else { - this.dom.results.innerHTML = '' + this.dom.results.textContent = '' } } } diff --git a/src/js/TreePath.js b/src/js/TreePath.js index 2fb9e3e0c..f5e3a9ce1 100644 --- a/src/js/TreePath.js +++ b/src/js/TreePath.js @@ -27,7 +27,7 @@ export class TreePath { * Reset component to initial status */ reset () { - this.path.innerHTML = translate('selectNode') + this.path.textContent = translate('selectNode') } /** @@ -38,7 +38,7 @@ export class TreePath { setPath (pathObjs) { const me = this - this.path.innerHTML = '' + this.path.textContent = '' if (pathObjs && pathObjs.length) { pathObjs.forEach((pathObj, idx) => { @@ -53,7 +53,7 @@ export class TreePath { if (pathObj.children.length) { sepEl = document.createElement('span') sepEl.className = 'jsoneditor-treepath-seperator' - sepEl.innerHTML = '►' + sepEl.textContent = '\u25BA' sepEl.onclick = () => { me.contentMenuClicked = true @@ -82,7 +82,7 @@ export class TreePath { const showAllBtn = document.createElement('span') showAllBtn.className = 'jsoneditor-treepath-show-all-btn' showAllBtn.title = 'show all path' - showAllBtn.innerHTML = '...' + showAllBtn.textContent = '...' showAllBtn.onclick = _onShowAllClick.bind(me, pathObjs) me.path.insertBefore(showAllBtn, me.path.firstChild) } diff --git a/src/js/appendNodeFactory.js b/src/js/appendNodeFactory.js index 459d56d47..76c01a4ad 100644 --- a/src/js/appendNodeFactory.js +++ b/src/js/appendNodeFactory.js @@ -64,7 +64,7 @@ export function appendNodeFactory (Node) { // a cell for the contents (showing text 'empty') const tdAppend = document.createElement('td') const domText = document.createElement('div') - domText.innerHTML = '(' + translate('empty') + ')' + domText.appendChild(document.createTextNode('(' + translate('empty') + ')')) domText.className = 'jsoneditor-readonly' tdAppend.appendChild(domText) dom.td = tdAppend @@ -100,7 +100,7 @@ export function appendNodeFactory (Node) { const domText = dom.text if (domText) { - domText.innerHTML = '(' + translate('empty') + ' ' + this.parent.type + ')' + domText.firstChild.nodeValue = '(' + translate('empty') + ' ' + this.parent.type + ')' } // attach or detach the contents of the append node: diff --git a/src/js/assets/selectr/selectr.js b/src/js/assets/selectr/selectr.js index 7452cf88b..5271e7b79 100644 --- a/src/js/assets/selectr/selectr.js +++ b/src/js/assets/selectr/selectr.js @@ -187,7 +187,7 @@ var util = { var i; for (i in a) if (i in el) el[i] = a[i]; - else if ("html" === i) el.innerHTML = a[i]; + else if ("html" === i) el.textContent = a[i]; else if ("text" === i) { var t = d.createTextNode(a[i]); el.appendChild(t); @@ -286,9 +286,6 @@ function appendItem(item, parent, custom) { } util.removeClass(item, "excluded"); - if (!custom) { - item.innerHTML = item.textContent; - } } /** @@ -829,7 +826,7 @@ var addTag = function(item) { docFrag.appendChild(tg); }); - this.label.innerHTML = ""; + this.label.textContent = ""; } else { docFrag.appendChild(tag); @@ -915,10 +912,6 @@ var clearSearch = function() { // Items that didn't match need the class // removing to make them visible again util.removeClass(item, "excluded"); - // Remove the span element for underlining matched items - if (!this.customOption) { - item.innerHTML = item.textContent; - } }, this); } }; @@ -1383,7 +1376,7 @@ Selectr.prototype.destroy = function() { } if (this.config.data) { - this.el.innerHTML = ""; + this.el.textContent = ""; } // Remove the className from select element @@ -1457,7 +1450,7 @@ Selectr.prototype.select = function(index) { addTag.call(this, item); } else { var data = this.data ? this.data[index] : option; - this.label.innerHTML = this.customSelected ? this.config.renderSelection(data) : option.textContent; + this.label.textContent = this.customSelected ? this.config.renderSelection(data) : option.textContent; this.selectedValue = option.value; this.selectedIndex = index; @@ -1519,7 +1512,7 @@ Selectr.prototype.deselect = function(index, force) { return false; } - this.label.innerHTML = ""; + this.label.textContent = ""; this.selectedValue = null; this.el.selectedIndex = this.selectedIndex = -1; @@ -1805,7 +1798,7 @@ Selectr.prototype.search = function(string) { // Underline the matching results if (!this.customOption) { - item.innerHTML = match(string, option); + item.textContent = match(string, option); } } else { util.addClass(item, "excluded"); @@ -2081,7 +2074,7 @@ Selectr.prototype.setPlaceholder = function(placeholder) { placeholder = "No options available"; } - this.placeEl.innerHTML = placeholder; + this.placeEl.textContent = placeholder; }; /** @@ -2119,7 +2112,7 @@ Selectr.prototype.setMessage = function(message, close) { */ Selectr.prototype.removeMessage = function() { util.removeClass(this.container, "notice"); - this.notice.innerHTML = ""; + this.notice.textContent = ""; }; /** diff --git a/src/js/autocomplete.js b/src/js/autocomplete.js index ada0663dd..8b9cae1b6 100644 --- a/src/js/autocomplete.js +++ b/src/js/autocomplete.js @@ -52,7 +52,7 @@ export function autocomplete (config) { refresh: function (token, array) { elem.style.visibility = 'hidden' ix = 0 - elem.innerHTML = '' + elem.textContent = '' const vph = (window.innerHeight || document.documentElement.clientHeight) const rect = elem.parentNode.getBoundingClientRect() const distanceToTop = rect.top - 6 // heuristic give 6px @@ -71,7 +71,11 @@ export function autocomplete (config) { divRow.onmouseout = onMouseOut divRow.onmousedown = onMouseDown divRow.__hint = row - divRow.innerHTML = row.substring(0, token.length) + '' + row.substring(token.length) + '' + divRow.textContent = '' + divRow.appendChild(document.createTextNode(row.substring(0, token.length))) + const b = document.createElement('b') + b.appendChild(document.createTextNode(row.substring(token.length))) + divRow.appendChild(b) elem.appendChild(divRow) return divRow }) @@ -153,13 +157,7 @@ export function autocomplete (config) { document.body.appendChild(spacer) } - // Used to encode an HTML string into a plain text. - // taken from http://stackoverflow.com/questions/1219860/javascript-jquery-html-encoding - spacer.innerHTML = String(text).replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>') + spacer.textContent = text return spacer.getBoundingClientRect().right } diff --git a/src/js/previewmode.js b/src/js/previewmode.js index 0c3f79d1f..49b29e08f 100644 --- a/src/js/previewmode.js +++ b/src/js/previewmode.js @@ -100,7 +100,7 @@ previewmode.create = function (container, options = {}) { this.dom.busy = document.createElement('div') this.dom.busy.className = 'jsoneditor-busy' this.dom.busyContent = document.createElement('span') - this.dom.busyContent.innerHTML = 'busy...' + this.dom.busyContent.textContent = 'busy...' this.dom.busy.appendChild(this.dom.busyContent) this.content.appendChild(this.dom.busy) diff --git a/src/js/showTransformModal.js b/src/js/showTransformModal.js index 5136c821e..ecc7c131c 100644 --- a/src/js/showTransformModal.js +++ b/src/js/showTransformModal.js @@ -136,7 +136,7 @@ export function showTransformModal ( if (!Array.isArray(value)) { wizard.style.fontStyle = 'italic' - wizard.innerHTML = '(wizard not available for objects, only for arrays)' + wizard.textContent = '(wizard not available for objects, only for arrays)' } const sortablePaths = getChildPaths(json) diff --git a/test/test_build.html b/test/test_build.html index 8945378c3..268137e9c 100644 --- a/test/test_build.html +++ b/test/test_build.html @@ -50,6 +50,7 @@ mode: 'tree', modes: ['code', 'form', 'text', 'tree', 'view', 'preview'], // allowed modes onError: function (err) { + console.error(err); alert(err.toString()); }, onChange: function () { @@ -88,7 +89,13 @@ "object": {"a": "b", "c": "d"}, "string": "Hello World", "timestamp": 1534952749890, - "url": "http://jsoneditoronline.org" + "url": "http://jsoneditoronline.org", + "": "xss?", + "xss array": [ + { + "": "xss?" + } + ] }; editorTest = new JSONEditor(container, options, json);