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:
- [CVE-2015-1866] Ember.js XSS Vulnerability With {{view "select"}} Options Matthew Beale (Apr 14)