CVE-2015-10089: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: A vulnerability classified as problematic has been found in flame.js. This affects an unknown part. The manipulation leads to cross site scripting. It is possible to initiate the attack remotely. This product does not use versioning. This is why information about affected and unaffected releases are unavailable. The patch is named e6c49b5f6179e31a534b7c3264e1d36aa99728ac. It is recommended to apply a patch to fix this issue. The associated identifier of this vulnerability is VDB-222291.
CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
CVSS v3 base score: 6.1
Published: 2023-03-05T14:15Z
Repository: https://github.com/flamejs/flame.js
Language: JavaScript
Files changed: utils/string_measurement.js, views/checkbox_view.js, views/menu_view.js, views/table_data_view.js, views/table_view.js
Commit date: 2015-04-22 12:57:28 +0300
Commit message: XSS fixes:
-Patch render calls in CheckboxView, TableView and TableDataView to display escaped content; preserve <br> tags in table headers in TableView
-Escape the innerHTML call in string_measurement.js
-Escape element.html call when selecting read-only content in TableDataView

Diff: diff --git a/utils/string_measurement.js b/utils/string_measurement.js
index d4ca5b7..02e3b61 100644
--- a/utils/string_measurement.js
+++ b/utils/string_measurement.js
@@ -17,13 +17,23 @@ Ember.mixin(Flame, {
         element.className = elementClasses;
         element.style.cssText = 'position:absolute; left: 0; top: 0; bottom: auto; right: auto; width: auto; height: auto;' + additionalStyles;
         return element;
     },
 
-    measureString: function(string, parentClasses, elementClasses, additionalStyles) {
+    measureString: function(stringOrArray, parentClasses, elementClasses, additionalStyles) {
+        var escape = Handlebars.Utils.escapeExpression;
+        var measuredString;
+        // We also accept an array of strings and then return the width of the longest one by joining them with <br>.
+        if (Ember.isArray(stringOrArray)) {
+            measuredString = stringOrArray.reduce(function(currentStrings, nextString) {
+                        return currentStrings + escape(nextString) + '<br>';
+                    }, '');
+        } else {
+            measuredString = escape(stringOrArray);
+        }
         var element = this._setupStringMeasurement(parentClasses, elementClasses, additionalStyles);
-        element.innerHTML = string;
+        element.innerHTML = measuredString;
         return {
             width: element.clientWidth,
             height: element.clientHeight
         };
     }
diff --git a/views/checkbox_view.js b/views/checkbox_view.js
index cb20fbf..151bfb5 100644
--- a/views/checkbox_view.js
+++ b/views/checkbox_view.js
@@ -7,11 +7,11 @@ Flame.CheckboxView = Flame.ButtonView.extend({
 
     render: function(buffer) {
         buffer.push('<div class="flame-checkbox-box"></div>');
         this.renderCheckMark(buffer);
         buffer.push('<label class="flame-checkbox-label">');
-        buffer.push(Ember.isNone(this.get('title')) ? '' : this.get('title'));
+        buffer.push(Ember.isNone(this.get('title')) ? '' : Handlebars.Utils.escapeExpression(this.get('title')));
         buffer.push('</label>');
     },
 
     renderCheckMark: function(buffer) {
         var imgUrl = Flame.image('checkmark.svg');
diff --git a/views/menu_view.js b/views/menu_view.js
index 2a59d31..ddc0019 100644
--- a/views/menu_view.js
+++ b/views/menu_view.js
@@ -101,14 +101,11 @@ Flame.MenuView = Flame.Panel.extend(Flame.ActionSupport, {
         var items = this.get('items') || [];
         if (Ember.get(items, 'length') === 0) {
             return;
         }
         var itemTitleKey = this.get('itemTitleKey');
-        var allTitles = items.reduce(function(currentTitles, item) {
-            var nextTitle = Ember.get(item, itemTitleKey);
-            return currentTitles + nextTitle + '<br>';
-        }, '');
+        var allTitles = items.map(function(currentTitles, item) { Ember.get(item, itemTitleKey); });
         // Give the menus a 16px breathing space to account for sub menu indicator, and to give some right margin (+18px for the padding)
         return Flame.measureString(allTitles, 'ember-view flame-view flame-list-item-view flame-menu-item-view', 'title').width + 16 + 18;
     },
 
     _createMenuItems: function() {
diff --git a/views/table_data_view.js b/views/table_data_view.js
index 0831c5e..a8bf946 100644
--- a/views/table_data_view.js
+++ b/views/table_data_view.js
@@ -454,11 +454,11 @@ Flame.TableDataView = Flame.View.extend(Flame.Statechart, {
 
         enterState: function() {
             var owner = this.get('owner');
             var selection = owner.get('selection');
             var dataCell = owner.get('selectedDataCell');
-            var readOnlyValue = owner.editableValue(dataCell, true);
+            var readOnlyValue = Handlebars.Utils.escapeExpression(owner.editableValue(dataCell, true));
             this.selectionContent = selection.html();
             selection.html(readOnlyValue);
             selection.addClass('read-only is-selectable');
         },
 
@@ -795,10 +795,11 @@ Flame.TableDataView = Flame.View.extend(Flame.Statechart, {
         var rowCount = data.length;
         var columnCount = data[0].length;
         var defaultCellWidth = this.get('parentView.defaultColumnWidth');
         var columnLeafs = this.get('parentView.content.columnLeafs');
         var cellWidth;
+        var escape = Handlebars.Utils.escapeExpression;
 
         var classes = 'flame-table';
         if (!this.get('parentView.allowSelection')) classes += ' is-selectable';
         var div = document.createElement('div');
         buffer.begin('table').attr('class', classes).attr('width', '1px');
@@ -816,14 +817,17 @@ Flame.TableDataView = Flame.View.extend(Flame.Statechart, {
                     content = (Ember.isNone(content) ? '' : content);
                     if (content instanceof HTMLElement || content instanceof DocumentFragment) {
                         while (div.firstChild) div.removeChild(div.firstChild);
                         div.appendChild(content);
                         content = div.innerHTML;
+                    } else {
+                        // Escape the cell content unless there is a deliberate implementation for cells with HTML content.
+                        content = escape(content);
                     }
                     cssClassesString = cell.cssClassesString();
                     if (cell.inlineStyles) inlineStyles = cell.inlineStyles();
-                    titleValue = (cell.titleValue && cell.titleValue() ? 'title="%@"'.fmt(cell.titleValue()) : '');
+                    titleValue = (cell.titleValue && cell.titleValue() ? 'title="%@"'.fmt(escape(cell.titleValue())) : '');
                 } else {
                     content = '<span style="color: #999">...</span>';
                 }
                 cellWidth = columnLeafs[j].get('render_width') || defaultCellWidth;
                 buffer.push('<td data-index="%@" class="%@" style="width: %@px; %@" %@>%@</td>'.fmt(
diff --git a/views/table_view.js b/views/table_view.js
index 7d05f54..2047913 100644
--- a/views/table_view.js
+++ b/views/table_view.js
@@ -317,11 +317,11 @@ Flame.TableView = Flame.View.extend(Flame.Statechart, {
         if (!headers) {
             return; // Nothing to render
         }
 
         if (this.get('content.title')) {
-            buffer.push('<div class="panel-title">%@</div>'.fmt(this.get('content.title')));
+            buffer.push('<div class="panel-title">%@</div>'.fmt(Handlebars.Utils.escapeExpression(this.get('content.title'))));
             didRenderTitle = true;
         }
 
         var defaultColumnWidth = this.get('defaultColumnWidth');
         var defaultRowHeaderWidth = this.get('rowHeaderWidth') || defaultColumnWidth;
@@ -436,11 +436,12 @@ Flame.TableView = Flame.View.extend(Flame.Statechart, {
             var header = row[i];
             buffer.begin('td');
 
             headerLabel = header.get ? header.get('headerLabel') : header.label;
             if (!headerLabel) headerLabel = "";
-
+            // We have to support <br> for row headers, so we'll replace them back after escaping
+            headerLabel = Ember.Handlebars.Utils.escapeExpression(headerLabel).replace(/&lt;br&gt;/g, '<br>');
             buffer.attr('title', headerLabel.replace(/<br>/g, '\n'));
 
             if (header.rowspan > 1) {
                 buffer.attr('rowspan', header.rowspan);
             }

Vulnerable code:     measureString: function(string, parentClasses, elementClasses, additionalStyles) {
        element.innerHTML = string;
        buffer.push(Ember.isNone(this.get('title')) ? '' : this.get('title'));
        var allTitles = items.reduce(function(currentTitles, item) {
            var nextTitle = Ember.get(item, itemTitleKey);
            return currentTitles + nextTitle + '<br>';
        }, '');
            var readOnlyValue = owner.editableValue(dataCell, true);
                    titleValue = (cell.titleValue && cell.titleValue() ? 'title="%@"'.fmt(cell.titleValue()) : '');
            buffer.push('<div class="panel-title">%@</div>'.fmt(this.get('content.title')));

Fixed code:     measureString: function(stringOrArray, parentClasses, elementClasses, additionalStyles) {
        var escape = Handlebars.Utils.escapeExpression;
        var measuredString;
        // We also accept an array of strings and then return the width of the longest one by joining them with <br>.
        if (Ember.isArray(stringOrArray)) {
            measuredString = stringOrArray.reduce(function(currentStrings, nextString) {
                        return currentStrings + escape(nextString) + '<br>';
                    }, '');
        } else {
            measuredString = escape(stringOrArray);
        }
        element.innerHTML = measuredString;
        buffer.push(Ember.isNone(this.get('title')) ? '' : Handlebars.Utils.escapeExpression(this.get('title')));
        var allTitles = items.map(function(currentTitles, item) { Ember.get(item, itemTitleKey); });
            var readOnlyValue = Handlebars.Utils.escapeExpression(owner.editableValue(dataCell, true));
        var escape = Handlebars.Utils.escapeExpression;
                    } else {
                        // Escape the cell content unless there is a deliberate implementation for cells with HTML content.
                        content = escape(content);
                    titleValue = (cell.titleValue && cell.titleValue() ? 'title="%@"'.fmt(escape(cell.titleValue())) : '');
            buffer.push('<div class="panel-title">%@</div>'.fmt(Handlebars.Utils.escapeExpression(this.get('content.title'))));
            // We have to support <br> for row headers, so we'll replace them back after escaping
            headerLabel = Ember.Handlebars.Utils.escapeExpression(headerLabel).replace(/&lt;br&gt;/g, '<br>');