Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Clean repeater list extension row content of XSS (fix #1973) #1974

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions js/repeater-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down
19 changes: 10 additions & 9 deletions js/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = $('<i>').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 = $('<i>').html(questionableMarkup).text();
}

return cleanedInput;
// string completely decoded now encode it
return $('<i>').text(questionableMarkup).html();
};

$.fn.utilities = {
Expand All @@ -79,4 +81,3 @@
// -- BEGIN UMD WRAPPER AFTERWORD --
}));
// -- END UMD WRAPPER AFTERWORD --

35 changes: 35 additions & 0 deletions test/repeater-list-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
} );
38 changes: 38 additions & 0 deletions test/xss-tools/jquery-xss-vectors.js
Original file line number Diff line number Diff line change
@@ -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"><script>alert(1)</script>"' },
{ item: 'break attribute', dirty: '"><script>alert(1)</script>' }
]);


return jqueryVectors;
});
63 changes: 63 additions & 0 deletions test/xss-tools/test-xss-vulnerabilities-in-jquery-markup.js
Original file line number Diff line number Diff line change
@@ -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();
}
};
}());
};
});
8 changes: 8 additions & 0 deletions test/xss-tools/xss-vectors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
define(function vectors () {
return [
{ item: 'script tags', dirty: '<script>alert("hack")</script>', clean: '&lt;script&gt;alert(&quot;hack&quot;)&lt;&#x2F;script&gt;' },
{ item: 'video tags', dirty: '<video/src="x"onloadstart="prompt()">', clean: '&lt;video&#x2F;src&#x3D;&quot;x&quot;onloadstart&#x3D;&quot;prompt()&quot;&gt;' },
{ item: 'tags', dirty: 'foo<&"\'>', clean: 'foo&lt;&amp;&quot;&#39;&gt;' },
{ item: 'equals', dirty: 'foo=', clean: 'foo&#x3D;' }
];
});