From 51529ca4c52ee8baa928f62549e2ff46ed662272 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 cd2dfc5..b25d9ae 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
@@ -310,14 +349,14 @@ Ember._RenderBuffer.prototype =
         style = this.elementStyle,
         attr, prop;
 
-    buffer += '<' + tagName;
+    buffer += '<' + stripTagName(tagName);
 
     if (id) {
-      buffer += ' id="' + this._escapeAttribute(id) + '"';
+      buffer += ' id="' + escapeAttribute(id) + '"';
       this.elementId = null;
     }
     if (classes) {
-      buffer += ' class="' + this._escapeAttribute(classes.join(' ')) + '"';
+      buffer += ' class="' + escapeAttribute(classes.join(' ')) + '"';
       this.classes = null;
     }
 
@@ -326,7 +365,7 @@ Ember._RenderBuffer.prototype =
 
       for (prop in style) {
         if (style.hasOwnProperty(prop)) {
-          buffer += prop + ':' + this._escapeAttribute(style[prop]) + ';';
+          buffer += prop + ':' + escapeAttribute(style[prop]) + ';';
         }
       }
 
@@ -338,7 +377,7 @@ Ember._RenderBuffer.prototype =
     if (attrs) {
       for (attr in attrs) {
         if (attrs.hasOwnProperty(attr)) {
-          buffer += ' ' + attr + '="' + this._escapeAttribute(attrs[attr]) + '"';
+          buffer += ' ' + attr + '="' + escapeAttribute(attrs[attr]) + '"';
         }
       }
 
@@ -353,7 +392,7 @@ Ember._RenderBuffer.prototype =
             if (value === true) {
               buffer += ' ' + prop + '="' + prop + '"';
             } else {
-              buffer += ' ' + prop + '="' + this._escapeAttribute(props[prop]) + '"';
+              buffer += ' ' + prop + '="' + escapeAttribute(props[prop]) + '"';
             }
           }
         }
@@ -368,7 +407,7 @@ Ember._RenderBuffer.prototype =
 
   pushClosingTag: function() {
     var tagName = this.tagNames.pop();
-    if (tagName) { this.buffer += '</' + tagName + '>'; }
+    if (tagName) { this.buffer += '</' + stripTagName(tagName) + '>'; }
   },
 
   currentTagName: function() {
@@ -466,30 +505,5 @@ Ember._RenderBuffer.prototype =
 
   innerString: function() {
     return this.buffer;
-  },
-
-  _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 f1ee93c..65ad714 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)

