From cc5b72f0c13b83cb451ec3da5a4d261c4dfd4a97 Mon Sep 17 00:00:00 2001
From: Yehuda Katz <wycats@gmail.com>
Date: Wed, 3 Jul 2013 14:23:22 -0700
Subject: [PATCH] Fix for Potential XSS Exploit When Binding to User-Supplied
 Data

---
 packages/ember-views/lib/system/render_buffer.js   | 78 +++++++++++++---------
 .../ember-views/tests/system/render_buffer_test.js | 13 ++++
 2 files changed, 59 insertions(+), 32 deletions(-)

diff --git a/packages/ember-views/lib/system/render_buffer.js b/packages/ember-views/lib/system/render_buffer.js
index f20e212..a33675a 100644
--- a/packages/ember-views/lib/system/render_buffer.js
+++ b/packages/ember-views/lib/system/render_buffer.js
@@ -23,6 +23,45 @@ ClassSet.prototype = {
   }
 };
 
+var BAD_TAG_NAME_TEST_REGEXP = /[^a-zA-Z\-]/;
+var BAD_TAG_NAME_REPLACE_REGEXP = /[^a-zA-Z\-]/g;
+
+function stripTagName(tagName) {
+  if (!tagName) {
+    return tagName;
+  }
+
+  if (!BAD_TAG_NAME_TEST_REGEXP.test(tagName)) {
+    return tagName;
+  }
+
+  return tagName.replace(BAD_TAG_NAME_REPLACE_REGEXP, '');
+}
+
+var BAD_CHARS_REGEXP = /&(?!\w+;)|[<>"'`]/g;
+var POSSIBLE_CHARS_REGEXP = /[&<>"'`]/;
+
+function escapeAttribute(value) {
+  // Stolen shamelessly from Handlebars
+
+  var escape = {
+    "<": "&lt;",
+    ">": "&gt;",
+    '"': "&quot;",
+    "'": "&#x27;",
+    "`": "&#x60;"
+  };
+
+  var escapeChar = function(chr) {
+    return escape[chr] || "&amp;";
+  };
+
+  var string = value.toString();
+
+  if(!POSSIBLE_CHARS_REGEXP.test(string)) { return string; }
+  return string.replace(BAD_CHARS_REGEXP, escapeChar);
+}
+
 /**
   `Ember.RenderBuffer` gathers information regarding the a view and generates the
   final representation. `Ember.RenderBuffer` will generate HTML which can be pushed
@@ -308,14 +347,14 @@ Ember._RenderBuffer.prototype =
         style = this.elementStyle,
         attr, prop;
 
-    buffer.push('<' + tagName);
+    buffer.push('<' + stripTagName(tagName));
 
     if (id) {
-      buffer.push(' id="' + this._escapeAttribute(id) + '"');
+      buffer.push(' id="' + escapeAttribute(id) + '"');
       this.elementId = null;
     }
     if (classes) {
-      buffer.push(' class="' + this._escapeAttribute(classes.join(' ')) + '"');
+      buffer.push(' class="' + escapeAttribute(classes.join(' ')) + '"');
       this.classes = null;
     }
 
@@ -324,7 +363,7 @@ Ember._RenderBuffer.prototype =
 
       for (prop in style) {
         if (style.hasOwnProperty(prop)) {
-          buffer.push(prop + ':' + this._escapeAttribute(style[prop]) + ';');
+          buffer.push(prop + ':' + escapeAttribute(style[prop]) + ';');
         }
       }
 
@@ -336,7 +375,7 @@ Ember._RenderBuffer.prototype =
     if (attrs) {
       for (attr in attrs) {
         if (attrs.hasOwnProperty(attr)) {
-          buffer.push(' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"');
+          buffer.push(' ' + attr + '="' + escapeAttribute(attrs[attr]) + '"');
         }
       }
 
@@ -351,7 +390,7 @@ Ember._RenderBuffer.prototype =
             if (value === true) {
               buffer.push(' ' + prop + '="' + prop + '"');
             } else {
-              buffer.push(' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"');
+              buffer.push(' ' + prop + '="' + escapeAttribute(props[prop]) + '"');
             }
           }
         }
@@ -365,7 +404,7 @@ Ember._RenderBuffer.prototype =
 
   pushClosingTag: function() {
     var tagName = this.tagNames.pop();
-    if (tagName) { this.buffer.push('</' + tagName + '>'); }
+    if (tagName) { this.buffer.push('</' + stripTagName(tagName) + '>'); }
   },
 
   currentTagName: function() {
@@ -460,30 +499,5 @@ Ember._RenderBuffer.prototype =
 
   innerString: function() {
     return this.buffer.join('');
-  },
-
-  _escapeAttribute: function(value) {
-    // Stolen shamelessly from Handlebars
-
-    var escape = {
-      "<": "&lt;",
-      ">": "&gt;",
-      '"': "&quot;",
-      "'": "&#x27;",
-      "`": "&#x60;"
-    };
-
-    var badChars = /&(?!\w+;)|[<>"'`]/g;
-    var possible = /[&<>"'`]/;
-
-    var escapeChar = function(chr) {
-      return escape[chr] || "&amp;";
-    };
-
-    var string = value.toString();
-
-    if(!possible.test(string)) { return string; }
-    return string.replace(badChars, escapeChar);
   }
-
 };
diff --git a/packages/ember-views/tests/system/render_buffer_test.js b/packages/ember-views/tests/system/render_buffer_test.js
index b738177..45a6b88 100644
--- a/packages/ember-views/tests/system/render_buffer_test.js
+++ b/packages/ember-views/tests/system/render_buffer_test.js
@@ -77,6 +77,19 @@ test("prevents XSS injection via `style`", function() {
   equal('<span></span><div style="color:blue;&quot; xss=&quot;true&quot; style=&quot;color:red;">', buffer.string());
 });
 
+test("prevents XSS injection via `tagName`", function() {
+  var buffer = new Ember.RenderBuffer('cool-div><div xss="true"');
+
+  buffer.push('<span></span>'); // We need the buffer to not be empty so we use the string path
+  buffer.pushOpeningTag();
+  buffer.begin('span><span xss="true"');
+  buffer.pushOpeningTag();
+  buffer.pushClosingTag();
+  buffer.pushClosingTag();
+
+  equal('<span></span><cool-divdivxsstrue><spanspanxsstrue></spanspanxsstrue></cool-divdivxsstrue>', buffer.string());
+});
+
 test("handles null props - Issue #2019", function() {
   var buffer = new Ember.RenderBuffer('div');
 
-- 
1.7.12.4 (Apple Git-37)

