oss-sec mailing list archives

[CVE-2015-1866] Ember.js XSS Vulnerability With {{view "select"}} Options


From: "Matthew Beale" <mbeale () nestlabs com>
Date: Tue, 14 Apr 2015 14:51:13 -0400

[CVE-2015-1866] Ember.js XSS Vulnerability With {{view "select"}} Options

Data passed as the label of select options may not be escaped before being passed to the browser.

* Versions Affected: 1.10.0, 1.11.0, 1.11.1, 1.12.0 beta
* Not affected: Versions prior to 1.10.0
* Fixed Versions: 1.10.1, 1.11.2

Impact
-------

In general, Ember.js escapes or strips any user-supplied content before inserting it in strings that will be sent to innerHTML. However, a change made to the implementation of the select view means that any user-supplied data bound to an option's label will not be escaped correctly.

In applications that use Ember's select view and pass user-supplied content to the label, a specially-crafted payload could execute arbitrary JavaScript in the context of the current domain ("XSS").

All users running an affected release and binding user-supplied data to the select options should either upgrade or use one of the workarounds immediately.

Releases
--------

Releases are available on emberjs.com/builds/#/tagged

Workarounds
-----------

Ensure that you escape any user-supplied value that you bind to an option label. For example, if you bind a label:

   {{view 'select' content=people optionLabelPath='content.name'}}

Ensure that you escape the `name` value of each item `people` using Ember.Handlebars.Utils.escapeExpression:

   var people = this.get('people');
   var peopleForSelect = people.map(function(person){
     var newPerson = Object.create(person);
     newPerson.name = Ember.Handlebars.escapeExpression(person.name);
     return newPerson;
   });
   this.set('peopleForSelect', peopleForSelect);

Credits
-------

This vulnerability was reported to us by Phillip Haines of Zestia. Many thanks for working with us on identifying the issue and on the advisory process.

Best,

-Matthew (Ember.js Core Team member)

http://madhatted.com :: @mixonic

diff --git a/packages/ember-htmlbars/lib/templates/select-option.hbs b/pa=
ckages/ember-htmlbars/lib/templates/select-option.hbs
new file mode 100644
index 0000000..6471e4e
--- /dev/null
+++ b/packages/ember-htmlbars/lib/templates/select-option.hbs
@@ -0,0 +1 @@
+{{~view.label~}}
diff --git a/packages/ember-views/lib/views/select.js b/packages/ember-vi=
ews/lib/views/select.js
index a68b58b..6a203ac 100644
--- a/packages/ember-views/lib/views/select.js
+++ b/packages/ember-views/lib/views/select.js
@@ -20,25 +20,12 @@ import { computed } from "ember-metal/computed";
import { A as emberA } from "ember-runtime/system/native_array";
import { observer } from "ember-metal/mixin";
import { defineProperty } from "ember-metal/properties";
-import run from "ember-metal/run_loop";
=

import htmlbarsTemplate from "ember-htmlbars/templates/select";
+import selectOptionDefaultTemplate from "ember-htmlbars/templates/select=
-option";
=

var defaultTemplate =3D htmlbarsTemplate;
=

-var selectOptionDefaultTemplate =3D {
-  isHTMLBars: true,
-  render: function(context, env, contextualElement) {
-    var lazyValue =3D context.getStream('view.label');
-
-    lazyValue.subscribe(context._wrapAsScheduled(function() {
-      run.scheduleOnce('render', context, 'rerender');
-    }));
-
-    return lazyValue.value();
-  }
-};
-
var SelectOption =3D View.extend({
  instrumentDisplay: 'Ember.SelectOption',
=

diff --git a/packages/ember-views/tests/views/select_test.js b/packages/e=
mber-views/tests/views/select_test.js
index 0452770..53762db 100644
--- a/packages/ember-views/tests/views/select_test.js
+++ b/packages/ember-views/tests/views/select_test.js
@@ -4,6 +4,7 @@ import run from "ember-metal/run_loop";
import jQuery from "ember-views/system/jquery";
import { map } from "ember-metal/enumerable_utils";
import EventDispatcher from "ember-views/system/event_dispatcher";
+import SafeString from 'htmlbars-util/safe-string';
=

var trim =3D jQuery.trim;
=

@@ -133,6 +134,44 @@ test("can specify the property path for an option's =
label and value", function()
  deepEqual(map(select.$('option').toArray(), function(el) { return jQue=
ry(el).attr('value'); }), ["1", "2"], "Options should have values");
});
=

+QUnit.test("XSS: does not escape label value when it is a SafeString", f=
unction() {
+  select.set('content', Ember.A([
+    { id: 1, firstName: new SafeString('<p>Yehuda</p>') },
+    { id: 2, firstName: new SafeString('<p>Tom</p>') }
+  ]));
+
+  select.set('optionLabelPath', 'content.firstName');
+  select.set('optionValuePath', 'content.id');
+
+  append();
+
+  equal(select.$('option').length, 2, "Should have two options");
+  equal(select.$('option[value=3D1] b').length, 1, "Should have child el=
ements");
+
+  // IE 8 adds whitespace
+  equal(trim(select.$().text()), "YehudaTom", "Options should have conte=
nt");
+  deepEqual(map(select.$('option').toArray(), function(el) { return jQue=
ry(el).attr('value'); }), ["1", "2"], "Options should have values");
+});
+
+QUnit.test("XSS: escapes label value content", function() {
+  select.set('content', Ember.A([
+    { id: 1, firstName: '<p>Yehuda</p>' },
+    { id: 2, firstName: '<p>Tom</p>' }
+  ]));
+
+  select.set('optionLabelPath', 'content.firstName');
+  select.set('optionValuePath', 'content.id');
+
+  append();
+
+  equal(select.$('option').length, 2, "Should have two options");
+  equal(select.$('option[value=3D1] b').length, 0, "Should have no child=
elements");
+
+  // IE 8 adds whitespace
+  equal(trim(select.$().text()), "<p>Yehuda</p><p>Tom</p>", "Options sho=
uld have content");
+  deepEqual(map(select.$('option').toArray(), function(el) { return jQue=
ry(el).attr('value'); }), ["1", "2"], "Options should have values");
+});
+
test("can retrieve the current selected option when multiple=3Dfalse", f=
unction() {
  var yehuda =3D { id: 1, firstName: 'Yehuda' };
  var tom =3D { id: 2, firstName: 'Tom' };


diff --git a/packages/ember-htmlbars/lib/templates/select-option.hbs b/pa=
ckages/ember-htmlbars/lib/templates/select-option.hbs
new file mode 100644
index 0000000..6471e4e
--- /dev/null
+++ b/packages/ember-htmlbars/lib/templates/select-option.hbs
@@ -0,0 +1 @@
+{{~view.label~}}
diff --git a/packages/ember-views/lib/views/select.js b/packages/ember-vi=
ews/lib/views/select.js
index 721da86..3583904 100644
--- a/packages/ember-views/lib/views/select.js
+++ b/packages/ember-views/lib/views/select.js
@@ -21,26 +21,12 @@ import { computed } from "ember-metal/computed";
import { A as emberA } from "ember-runtime/system/native_array";
import { observer } from "ember-metal/mixin";
import { defineProperty } from "ember-metal/properties";
-import run from "ember-metal/run_loop";
=

import htmlbarsTemplate from "ember-htmlbars/templates/select";
+import selectOptionDefaultTemplate from "ember-htmlbars/templates/select=
-option";
=

var defaultTemplate =3D htmlbarsTemplate;
=

-var selectOptionDefaultTemplate =3D {
-  isHTMLBars: true,
-  revision: 'Ember@VERSION_STRING_PLACEHOLDER',
-  render: function(context, env, contextualElement) {
-    var lazyValue =3D context.getStream('view.label');
-
-    lazyValue.subscribe(context._wrapAsScheduled(function() {
-      run.scheduleOnce('render', context, 'rerender');
-    }));
-
-    return lazyValue.value();
-  }
-};
-
var SelectOption =3D View.extend({
  instrumentDisplay: 'Ember.SelectOption',
=

diff --git a/packages/ember-views/tests/views/select_test.js b/packages/e=
mber-views/tests/views/select_test.js
index eda11bd..8150e31 100644
--- a/packages/ember-views/tests/views/select_test.js
+++ b/packages/ember-views/tests/views/select_test.js
@@ -4,6 +4,7 @@ import run from "ember-metal/run_loop";
import jQuery from "ember-views/system/jquery";
import { map } from "ember-metal/enumerable_utils";
import EventDispatcher from "ember-views/system/event_dispatcher";
+import SafeString from 'htmlbars-util/safe-string';
=

var trim =3D jQuery.trim;
=

@@ -133,6 +134,44 @@ QUnit.test("can specify the property path for an opt=
ion's label and value", func
  deepEqual(map(select.$('option').toArray(), function(el) { return jQue=
ry(el).attr('value'); }), ["1", "2"], "Options should have values");
});
=

+QUnit.test("XSS: does not escape label value when it is a SafeString", f=
unction() {
+  select.set('content', Ember.A([
+    { id: 1, firstName: new SafeString('<p>Yehuda</p>') },
+    { id: 2, firstName: new SafeString('<p>Tom</p>') }
+  ]));
+
+  select.set('optionLabelPath', 'content.firstName');
+  select.set('optionValuePath', 'content.id');
+
+  append();
+
+  equal(select.$('option').length, 2, "Should have two options");
+  equal(select.$('option[value=3D1] b').length, 1, "Should have child el=
ements");
+
+  // IE 8 adds whitespace
+  equal(trim(select.$().text()), "YehudaTom", "Options should have conte=
nt");
+  deepEqual(map(select.$('option').toArray(), function(el) { return jQue=
ry(el).attr('value'); }), ["1", "2"], "Options should have values");
+});
+
+QUnit.test("XSS: escapes label value content", function() {
+  select.set('content', Ember.A([
+    { id: 1, firstName: '<p>Yehuda</p>' },
+    { id: 2, firstName: '<p>Tom</p>' }
+  ]));
+
+  select.set('optionLabelPath', 'content.firstName');
+  select.set('optionValuePath', 'content.id');
+
+  append();
+
+  equal(select.$('option').length, 2, "Should have two options");
+  equal(select.$('option[value=3D1] b').length, 0, "Should have no child=
elements");
+
+  // IE 8 adds whitespace
+  equal(trim(select.$().text()), "<p>Yehuda</p><p>Tom</p>", "Options sho=
uld have content");
+  deepEqual(map(select.$('option').toArray(), function(el) { return jQue=
ry(el).attr('value'); }), ["1", "2"], "Options should have values");
+});
+
QUnit.test("can retrieve the current selected option when multiple=3Dfal=
se", function() {
  var yehuda =3D { id: 1, firstName: 'Yehuda' };
  var tom =3D { id: 2, firstName: 'Tom' };


diff --git a/packages/ember-htmlbars/lib/templates/select-option.hbs b/pa=
ckages/ember-htmlbars/lib/templates/select-option.hbs
new file mode 100644
index 0000000..6471e4e
--- /dev/null
+++ b/packages/ember-htmlbars/lib/templates/select-option.hbs
@@ -0,0 +1 @@
+{{~view.label~}}
diff --git a/packages/ember-views/lib/views/select.js b/packages/ember-vi=
ews/lib/views/select.js
index f5de69d..191e813 100644
--- a/packages/ember-views/lib/views/select.js
+++ b/packages/ember-views/lib/views/select.js
@@ -21,26 +21,12 @@ import { computed } from "ember-metal/computed";
import { A as emberA } from "ember-runtime/system/native_array";
import { observer } from "ember-metal/mixin";
import { defineProperty } from "ember-metal/properties";
-import run from "ember-metal/run_loop";
=

import htmlbarsTemplate from "ember-htmlbars/templates/select";
+import selectOptionDefaultTemplate from "ember-htmlbars/templates/select=
-option";
=

var defaultTemplate =3D htmlbarsTemplate;
=

-var selectOptionDefaultTemplate =3D {
-  isHTMLBars: true,
-  revision: 'Ember@VERSION_STRING_PLACEHOLDER',
-  render(context, env, contextualElement) {
-    var lazyValue =3D context.getStream('view.label');
-
-    lazyValue.subscribe(context._wrapAsScheduled(function() {
-      run.scheduleOnce('render', context, 'rerender');
-    }));
-
-    return lazyValue.value();
-  }
-};
-
var SelectOption =3D View.extend({
  instrumentDisplay: 'Ember.SelectOption',
=

diff --git a/packages/ember-views/tests/views/select_test.js b/packages/e=
mber-views/tests/views/select_test.js
index d9fb500..af50933 100644
--- a/packages/ember-views/tests/views/select_test.js
+++ b/packages/ember-views/tests/views/select_test.js
@@ -4,6 +4,7 @@ import run from "ember-metal/run_loop";
import jQuery from "ember-views/system/jquery";
import { map } from "ember-metal/enumerable_utils";
import EventDispatcher from "ember-views/system/event_dispatcher";
+import SafeString from 'htmlbars-util/safe-string';
=

var trim =3D jQuery.trim;
=

@@ -133,6 +134,44 @@ QUnit.test("can specify the property path for an opt=
ion's label and value", func
  deepEqual(map(select.$('option').toArray(), function(el) { return jQue=
ry(el).attr('value'); }), ["1", "2"], "Options should have values");
});
=

+QUnit.test("XSS: does not escape label value when it is a SafeString", f=
unction() {
+  select.set('content', Ember.A([
+    { id: 1, firstName: new SafeString('<p>Yehuda</p>') },
+    { id: 2, firstName: new SafeString('<p>Tom</p>') }
+  ]));
+
+  select.set('optionLabelPath', 'content.firstName');
+  select.set('optionValuePath', 'content.id');
+
+  append();
+
+  equal(select.$('option').length, 2, "Should have two options");
+  equal(select.$('option[value=3D1] b').length, 1, "Should have child el=
ements");
+
+  // IE 8 adds whitespace
+  equal(trim(select.$().text()), "YehudaTom", "Options should have conte=
nt");
+  deepEqual(map(select.$('option').toArray(), function(el) { return jQue=
ry(el).attr('value'); }), ["1", "2"], "Options should have values");
+});
+
+QUnit.test("XSS: escapes label value content", function() {
+  select.set('content', Ember.A([
+    { id: 1, firstName: '<p>Yehuda</p>' },
+    { id: 2, firstName: '<p>Tom</p>' }
+  ]));
+
+  select.set('optionLabelPath', 'content.firstName');
+  select.set('optionValuePath', 'content.id');
+
+  append();
+
+  equal(select.$('option').length, 2, "Should have two options");
+  equal(select.$('option[value=3D1] b').length, 0, "Should have no child=
elements");
+
+  // IE 8 adds whitespace
+  equal(trim(select.$().text()), "<p>Yehuda</p><p>Tom</p>", "Options sho=
uld have content");
+  deepEqual(map(select.$('option').toArray(), function(el) { return jQue=
ry(el).attr('value'); }), ["1", "2"], "Options should have values");
+});
+
QUnit.test("can retrieve the current selected option when multiple=3Dfal=
se", function() {
  var yehuda =3D { id: 1, firstName: 'Yehuda' };
  var tom =3D { id: 2, firstName: 'Tom' };

Current thread: