diff --git a/js/repeater-list.js b/js/repeater-list.js index 90377631b..8dfc1cda3 100755 --- a/js/repeater-list.js +++ b/js/repeater-list.js @@ -16,18 +16,23 @@ (function UMDFactory (factory) { if (typeof define === 'function' && define.amd) { // if AMD loader is available, register as an anonymous module. - define(['jquery', 'fuelux/repeater', 'fuelux/checkbox'], factory); + define(['jquery', 'fuelux/utilities', 'fuelux/repeater', 'fuelux/checkbox'], factory); } else if (typeof exports === 'object') { // Node/CommonJS - module.exports = factory(require('jquery'), require('./repeater'), require('./checkbox')); + module.exports = factory(require('jquery'), require('./utilities'), require('./repeater'), require('./checkbox')); } else { // OR use browser globals if AMD is not present factory(jQuery); } }(function repeaterList ($) { + if (!$.fn.utilities) { + throw new Error('Fuel UX repeater control list extension requires FuelUX utilities.'); + } // -- END UMD WRAPPER PREFACE -- // -- BEGIN MODULE CODE HERE -- + var utilities = $.fn.utilities; + var cleanInput = utilities.cleanInput; if ($.fn.repeater) { // ADDITIONAL METHODS @@ -646,7 +651,7 @@ content = (content !== undefined) ? content : ''; - $col.addClass(((className !== undefined) ? className : '')).append(content); + $col.addClass(((className !== undefined) ? className : '')).append(cleanInput(content)); if (width !== undefined) { $col.outerWidth(width); } diff --git a/js/utilities.js b/js/utilities.js index d0a305b65..f9bb82ef5 100644 --- a/js/utilities.js +++ b/js/utilities.js @@ -53,16 +53,18 @@ var isUpArrow = isKey(CONST.UP_ARROW_KEYCODE); var isDownArrow = isKey(CONST.DOWN_ARROW_KEYCODE); - // https://github.com/ExactTarget/fuelux/issues/1841 - var xssRegex = /<.*>/; - var cleanInput = function cleanInput (questionableInput) { - var cleanedInput = questionableInput; - - if (xssRegex.test(cleanedInput)) { - cleanedInput = $('').text(questionableInput).html(); + var ENCODED_REGEX = /&[^\s]*;/; + /* + * to prevent double encoding decodes content in loop until content is encoding free + */ + var cleanInput = function cleanInput (questionableMarkup) { + // check for encoding and decode + while (ENCODED_REGEX.test(questionableMarkup)) { + questionableMarkup = $('').html(questionableMarkup).text(); } - return cleanedInput; + // string completely decoded now encode it + return $('').text(questionableMarkup).html(); }; $.fn.utilities = { @@ -79,4 +81,3 @@ // -- BEGIN UMD WRAPPER AFTERWORD -- })); // -- END UMD WRAPPER AFTERWORD -- - diff --git a/test/repeater-list-test.js b/test/repeater-list-test.js index c8a4b7b9d..ca0b25d64 100644 --- a/test/repeater-list-test.js +++ b/test/repeater-list-test.js @@ -450,4 +450,39 @@ define( function repeaterListTest ( require ) { $repeater.repeater( repeaterOptions ); } ); + + + QUnit.test('Fuel Repeater List plugin is XSS free', function(assert) { + var ready = assert.async(); + var exposedAttributes = [ + 'dataSource.property.value' + ]; + + var CreateVulnarabilityTest = require('xss-tools/test-xss-vulnerabilities-in-jquery-markup.js'); + var testVulnarabilities = new CreateVulnarabilityTest({ + attributes: exposedAttributes, + buildControlContainer: function selectBoxBuildControl(vulnerability, attribute) { + var $controlContainer = this.$markup.clone().repeater({ + dataSource: makeDataSource([{ + label: 'test', + property: 'test' + }], [{ + test: vulnerability.dirty + }]) + }); + $('body').append($controlContainer); + + return $controlContainer.find('.repeater-viewport'); + }.bind(this), + destroyControlContainer: function($controlContainerViewport) { + var $controlContainer = $controlContainerViewport.closest('.repeater'); + $controlContainer.repeater('destroy'); + $controlContainer.remove(); + } + }); + + testVulnarabilities(function() { + ready(); + }); + }); } ); diff --git a/test/xss-tools/jquery-xss-vectors.js b/test/xss-tools/jquery-xss-vectors.js new file mode 100644 index 000000000..b1729e451 --- /dev/null +++ b/test/xss-tools/jquery-xss-vectors.js @@ -0,0 +1,38 @@ +define(function jqueryVectors (require) { + var vectors = require('xss-tools/xss-vectors.js'); + + var jqueryVectors = []; + var vectorsToMap = [].concat(vectors); + + function mapVectorsForJquery(vector) { + var newVector = Object.create(vector); + + // exclude vectors known to be safe in jquery + var jquerySafeVecortsDirtyCode = { + 'foo=': 1 + }; + if (jquerySafeVecortsDirtyCode[newVector.dirty]) { + return; + } + + /* + * jquery encodes tag attributes differntly from markup + * looking for clean is less consistent/feasible + * opting to look for dirty instead therefor these vector attribues are not needed + */ + newVector.clean = undefined; + newVector.regexClean = undefined; + + jqueryVectors.push(newVector); + } + + vectorsToMap.forEach(mapVectorsForJquery); + + jqueryVectors = jqueryVectors.concat([ + { item: '[w-3766576] Store XSS', dirty: 'TestTriggerSe"name="txtName">"' }, + { item: 'break attribute', dirty: '">' } + ]); + + + return jqueryVectors; +}); diff --git a/test/xss-tools/test-xss-vulnerabilities-in-jquery-markup.js b/test/xss-tools/test-xss-vulnerabilities-in-jquery-markup.js new file mode 100644 index 000000000..1a479a6da --- /dev/null +++ b/test/xss-tools/test-xss-vulnerabilities-in-jquery-markup.js @@ -0,0 +1,63 @@ +define(function testXssVulnerabilitiesInJqueryMarkup (require) { + var vulnerabilities = require('xss-tools/jquery-xss-vectors.js'); + + return function createVulnarabilityTest(config) { + return (function curryTestVulnerability() { + var attributes = config.attributes; + var buildControlContainer = config.buildControlContainer; + var destroyControlContainer = config.destroyControlContainer; + + /* + * with all the randomness jquery does to markup + * how do you determine the code is safe? + * + * cleaned pattern isn't necessary to check because + * + * if dirty pattern is not found it is safe + * if the dirty pattern is found + * but is an attibute value it is safe + */ + function testVulnarability(vulnerability, attribute) { + var isSafe = false; + + var $controlContainer = buildControlContainer(vulnerability, attribute); + var renderedHtml = $controlContainer.html(); + + var escapedDirty = vulnerability.dirty.replace(/([\(\)\\])/g, '\\$1'); + var regexDirty = new RegExp(escapedDirty); + var isDirty = regexDirty.test(renderedHtml); + var dirtyMatches = renderedHtml.match(regexDirty) || []; + + var regexDirtyInAttributeValue = new RegExp('\\w*="'+ escapedDirty + '"'); + var isDirtyInAttributeValue = regexDirtyInAttributeValue.test(renderedHtml); + var safeDirtyMatches = renderedHtml.match(regexDirtyInAttributeValue) || []; + + if (dirtyMatches.length === 0 || dirtyMatches.length === safeDirtyMatches.length) { + isSafe = true; + } + + QUnit.assert.ok(isSafe, '' + + 'safe from "' + vulnerability.item + '" attack using "' + attribute + '" attribute' + + '\n rendered html: ' + + '\n ' + renderedHtml + + '\n dirty html: ' + + '\n ' + vulnerability.dirty + + ''); + + destroyControlContainer($controlContainer); + } + + return function runVulnerabilityTests(completeCallback) { + attributes.forEach(function testAttribute(attribute) { + vulnerabilities.forEach(function(vulnerability) { + testVulnarability(vulnerability, attribute); + }); + }); + + if (typeof completeCallback === 'function') { + completeCallback(); + } + }; + }()); + }; +}); diff --git a/test/xss-tools/xss-vectors.js b/test/xss-tools/xss-vectors.js new file mode 100644 index 000000000..c8948b866 --- /dev/null +++ b/test/xss-tools/xss-vectors.js @@ -0,0 +1,8 @@ +define(function vectors () { + return [ + { item: 'script tags', dirty: '', clean: '<script>alert("hack")</script>' }, + { item: 'video tags', dirty: '