CVE-2017-16015: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: Forms is a library for easily creating HTML forms. Versions before 1.3.0 did not have proper html escaping. This means that if the application did not sanitize html on behalf of forms, use of forms may be vulnerable to cross site scripting
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: 2018-06-04T19:29Z
Repository: https://github.com/caolan/forms
Language: JavaScript
Files changed: .eslintrc, lib/render.js, lib/tag.js, lib/widgets.js, test/test-widgets.js
Commit date: 2016-11-15 18:01:48 -0800
Commit message: [New] [Fix] ensure that all content in `tag` is properly escaped.
Diff: diff --git a/.eslintrc b/.eslintrc
index e28dafb..3ba77f8 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -11,11 +11,11 @@
 		"eqeqeq": [2, "allow-null"],
 		"id-length": [2, { "min": 1, "max": 30 }],
 		"indent": [2, 4],
 		"max-lines": 0,
 		"max-nested-callbacks": [2, 5],
-		"max-params": [2, 3],
+		"max-params": [2, 4],
 		"max-statements": [2, 20],
 		"max-statements-per-line": [2, { "max": 2 }],
 		"new-cap": [2, { "capIsNewExceptions": ["Template"] }],
 		"no-extra-parens": [0],
 		"no-magic-numbers": [0],
diff --git a/lib/render.js b/lib/render.js
index 697ddff..fb8ef76 100644
--- a/lib/render.js
+++ b/lib/render.js
@@ -19,11 +19,11 @@ var wrapWith = function (tagName) {
             var fieldset = tag('fieldset', fieldsetAttrs, [
                 tag('legend', legendAttrs, field.labelText(name)),
                 opt.errorAfterField ? '' : errorHTML,
                 field.widget.toHTML(name, field),
                 opt.errorAfterField ? errorHTML : ''
-            ].join(''));
+            ].join(''), true);
             wrappedContent.push(fieldset);
         } else {
             var fieldHTMLs = [field.labelHTML(name, field.id), field.widget.toHTML(name, field)];
             if (opt.labelAfterField) {
                 fieldHTMLs.reverse();
@@ -33,21 +33,21 @@ var wrapWith = function (tagName) {
             } else {
                 fieldHTMLs.unshift(errorHTML);
             }
             wrappedContent = wrappedContent.concat(fieldHTMLs);
         }
-        return tag(tagName, { classes: field.classes() }, wrappedContent.join(''));
+        return tag(tagName, { classes: field.classes() }, wrappedContent.join(''), true);
     };
 };
 exports.div = wrapWith('div');
 exports.p = wrapWith('p');
 exports.li = wrapWith('li');
 
 exports.table = function (name, field, options) {
     var opt = options || {};
 
-    var th = tag('th', {}, field.labelHTML(name, field.id));
+    var th = tag('th', {}, field.labelHTML(name, field.id), true);
 
     var tdContent = field.widget.toHTML(name, field);
 
     if (!opt.hideError) {
         var errorHTML = field.errorHTML();
@@ -56,9 +56,9 @@ exports.table = function (name, field, options) {
         } else {
             tdContent = errorHTML + tdContent;
         }
     }
 
-    var td = tag('td', {}, tdContent);
+    var td = tag('td', {}, tdContent, true);
 
-    return tag('tr', { classes: field.classes() }, th + td);
+    return tag('tr', { classes: field.classes() }, th + td, true);
 };
diff --git a/lib/tag.js b/lib/tag.js
index 7f7643c..7dbe7a1 100644
--- a/lib/tag.js
+++ b/lib/tag.js
@@ -48,16 +48,17 @@ var selfClosingTags = {
 };
 var isSelfClosing = function (tagName) {
     return Object.prototype.hasOwnProperty.call(selfClosingTags, tagName);
 };
 
-var tag = function tag(tagName, attrsMap, content) {
+var tag = function tag(tagName, attrsMap, content, contentIsEscaped) {
     var safeTagName = htmlEscape(tagName);
     var attrsHTML = !is.array(attrsMap) ? attrs(attrsMap) : attrsMap.reduce(function (html, map) {
         return html + attrs(map);
     }, '');
-    return '<' + safeTagName + attrsHTML + (isSelfClosing(safeTagName) ? ' />' : '>' + content + '</' + safeTagName + '>');
+    var safeContent = contentIsEscaped ? content : htmlEscape(content);
+    return '<' + safeTagName + attrsHTML + (isSelfClosing(safeTagName) ? ' />' : '>' + safeContent + '</' + safeTagName + '>');
 };
 
 tag.attrs = attrs;
 
 module.exports = tag;
diff --git a/lib/widgets.js b/lib/widgets.js
index a8235db..c413d03 100644
--- a/lib/widgets.js
+++ b/lib/widgets.js
@@ -115,11 +115,11 @@ var select = function (isMultiple) {
         w.toHTML = function (name, field) {
             var f = field || {};
             var choices = unifyChoices(f.choices, 1);
             var optionsHTML = renderChoices(choices, function render(choice) {
                 if (choice.isNested) {
-                    return tag('optgroup', { label: choice.label }, renderChoices(choice.choices, render));
+                    return tag('optgroup', { label: choice.label }, renderChoices(choice.choices, render), true);
                 } else {
                     return tag('option', { value: choice.value, selected: !!isSelected(f.value, choice.value) }, choice.label);
                 }
             });
             var attrs = {
@@ -128,11 +128,11 @@ var select = function (isMultiple) {
                 classes: w.classes
             };
             if (isMultiple) {
                 attrs.multiple = true;
             }
-            return tag('select', [attrs, userAttrs, w.attrs || {}], optionsHTML);
+            return tag('select', [attrs, userAttrs, w.attrs || {}], optionsHTML, true);
         };
         return w;
     };
 };
 
diff --git a/test/test-widgets.js b/test/test-widgets.js
index ce4ef75..0f9021f 100644
--- a/test/test-widgets.js
+++ b/test/test-widgets.js
@@ -254,10 +254,20 @@ test('textarea', function (t) {
             rows: 20
         }).toHTML('name', { id: 'someid', value: 'value' }),
         '<textarea name="name" id="someid" rows="20" cols="80" class="one two" placeholder="hi!">value</textarea>'
     );
     t.equal(forms.widgets.textarea().type, 'textarea');
+
+    t.test('properly escapes contents', function (st) {
+        st.equal(
+            forms.widgets.textarea().toHTML('name', { value: 'Inside</textarea>Escaped the textarea!' }),
+            '<textarea name="name" id="id_name">Inside&lt;/textarea&gt;Escaped the textarea!</textarea>'
+       );
+
+        st.end();
+    });
+
     t.end();
 });
 
 test('multipleCheckbox', function (t) {
     var w = forms.widgets.multipleCheckbox();

Vulnerable code: 		"max-params": [2, 3],
            ].join(''));
        return tag(tagName, { classes: field.classes() }, wrappedContent.join(''));
    var th = tag('th', {}, field.labelHTML(name, field.id));
    var td = tag('td', {}, tdContent);
    return tag('tr', { classes: field.classes() }, th + td);
var tag = function tag(tagName, attrsMap, content) {
    return '<' + safeTagName + attrsHTML + (isSelfClosing(safeTagName) ? ' />' : '>' + content + '</' + safeTagName + '>');
                    return tag('optgroup', { label: choice.label }, renderChoices(choice.choices, render));
            return tag('select', [attrs, userAttrs, w.attrs || {}], optionsHTML);
Fixed code: 		"max-params": [2, 4],
            ].join(''), true);
        return tag(tagName, { classes: field.classes() }, wrappedContent.join(''), true);
    var th = tag('th', {}, field.labelHTML(name, field.id), true);
    var td = tag('td', {}, tdContent, true);
    return tag('tr', { classes: field.classes() }, th + td, true);
var tag = function tag(tagName, attrsMap, content, contentIsEscaped) {
    var safeContent = contentIsEscaped ? content : htmlEscape(content);
    return '<' + safeTagName + attrsHTML + (isSelfClosing(safeTagName) ? ' />' : '>' + safeContent + '</' + safeTagName + '>');
                    return tag('optgroup', { label: choice.label }, renderChoices(choice.choices, render), true);
            return tag('select', [attrs, userAttrs, w.attrs || {}], optionsHTML, true);

    t.test('properly escapes contents', function (st) {
        st.equal(
            forms.widgets.textarea().toHTML('name', { value: 'Inside</textarea>Escaped the textarea!' }),
            '<textarea name="name" id="id_name">Inside&lt;/textarea&gt;Escaped the textarea!</textarea>'
       );

        st.end();
    });