diff --git a/README.md b/README.md index a06b801..9269a75 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ and retrieve your data with `document.getElementById('strJS').value`. ### The API -There are five context-sensitive filters for generic input: +There are five context-sensitive filters for textual input: - `
` `{{{inHTMLData data}}}` `
` - `` - `` diff --git a/src/xss-filters.js b/src/xss-filters.js index abfe564..75d49c1 100644 --- a/src/xss-filters.js +++ b/src/xss-filters.js @@ -20,12 +20,14 @@ exports._getPrivFilters = function () { SPECIAL_HTML_CHARS = /[&<>"'`]/g, SPECIAL_COMMENT_CHARS = /(?:\x00|^-*!?>|--!?>|--?!?$|\]>|\]$)/g; - // CSS sensitive chars: ()"'/,!*@{}:; - // By CSS: (Tab|NewLine|colon|semi|lpar|rpar|apos|sol|comma|excl|ast|midast);|(quot|QUOT) - // By URI_PROTOCOL: (Tab|NewLine); + // Only a limited set of named references require decoding : + // for CSS: (Tab|NewLine|colon|semi|lpar|rpar|apos|sol|comma|excl|ast|midast);|(nbsp|quot|QUOT);? // ref: PPSE-1742 + // for URI: (Tab|NewLine); // colon; is decoded by URI_PROTOCOL_COLON + // for generic html decoding: (apos;|(lt|LT|gt|GT|amp|AMP|quot|QUOT);?) var SENSITIVE_HTML_ENTITIES = /&(?:#([xX][0-9A-Fa-f]+|\d+);?|(Tab|NewLine|colon|semi|lpar|rpar|apos|sol|comma|excl|ast|midast|ensp|emsp|thinsp);|(nbsp|amp|AMP|lt|LT|gt|GT|quot|QUOT);?)/g, - SENSITIVE_NAMED_REF_MAP = {Tab: '\t', NewLine: '\n', colon: ':', semi: ';', lpar: '(', rpar: ')', apos: '\'', sol: '/', comma: ',', excl: '!', ast: '*', midast: '*', ensp: '\u2002', emsp: '\u2003', thinsp: '\u2009', nbsp: '\xA0', amp: '&', lt: '<', gt: '>', quot: '"', QUOT: '"'}; + SENSITIVE_NAMED_REF_MAP = {Tab: '\t', NewLine: '\n', colon: ':', semi: ';', lpar: '(', rpar: ')', apos: '\'', sol: '/', comma: ',', excl: '!', ast: '*', midast: '*', ensp: '\u2002', emsp: '\u2003', thinsp: '\u2009', nbsp: '\xA0', amp: '&', AMP: '&', lt: '<', LT: '<', gt: '>', GT: '>', quot: '"', QUOT: '"'}; + // CSS sensitive chars: ()"'/,!*@{}:; // var CSS_VALID_VALUE = // /^(?: // (?!-*expression)#?[-\w]+ @@ -61,15 +63,17 @@ exports._getPrivFilters = function () { URI_PROTOCOL_NAMED_REF_MAP = {Tab: '\t', NewLine: '\n'}; var x, - strReplace = function (s, regexp, callback) { + _strReplace = function (s, regexp, callback) { return s === undefined ? 'undefined' - : s === null ? 'null' + : s === null ? 'null' : s.toString().replace(regexp, callback); }, + // only the five basic contextual filters yd, yc, yavu, yavs, yavd will be relying on strReplace + strReplace = _strReplace, fromCodePoint = String.fromCodePoint || function(codePoint) { - if (arguments.length === 0) { - return ''; - } + // the following is dead code as we always provide codePoint + // if (arguments.length === 0) { return ''; } + if (codePoint <= 0xFFFF) { // BMP code point return String.fromCharCode(codePoint); } @@ -80,6 +84,29 @@ exports._getPrivFilters = function () { return String.fromCharCode((codePoint >> 10) + 0xD800, (codePoint % 0x400) + 0xDC00); }; + // patch document.write() and document.writeln() to properly handle NULL for IE 9 or below + /*jshint -W030 */ + typeof document !== 'undefined' && function () { + var doc=document,b=doc.createElement('b'),w=doc.write,wl=doc.writeln, patch; + b.innerHTML='\x001'; + if (!b.innerHTML.length && w) { + patch = function(original) { + return function() { + var args = arguments, i = 0, len = args.length, s; + // replace every NULL char with \uFFFD in every argument + for (; i < len; i++) { + if (typeof (s = args[i]) === 'string') { + args[i] = s.replace(NULL, '\uFFFD'); + } + } + return Function.prototype.apply.call(original, doc, args); + }; + }; + /*jshint -W030 */ + doc.write = patch(w); + doc.writeln = patch(wl); + } + }(); function getProtocol(s) { s = s.split(URI_PROTOCOL_COLON, 2); @@ -157,7 +184,7 @@ exports._getPrivFilters = function () { : (num >= 0xD800 && num <= 0xDFFF) || num === 0x0D ? '\uFFFD' : x.frCoPt(num); } - return namedRefMap[named || named1] || m; + return namedRefMap[named || named1]; } return s === undefined ? 'undefined' @@ -182,6 +209,20 @@ exports._getPrivFilters = function () { } return (x = { + config: function(options) { + options = options || {}; + + if (options.replaceNull === true) { + // change strReplace so that it always replace NULL with \uFFFD at last if any + strReplace = function (s, regexp, callback) { + return s === undefined ? 'undefined' + : s === null ? 'null' + : s.toString().replace(regexp, callback).replace(NULL, '\uFFFD'); + }; + } else if (options.replaceNull === false) { + strReplace = _strReplace; + } + }, // turn invalid codePoints and that of non-characters to \uFFFD, and then fromCodePoint() frCoPt: function(num) { return num === undefined || num === null ? '' : @@ -217,7 +258,7 @@ exports._getPrivFilters = function () { * */ y: function(s) { - return strReplace(s, SPECIAL_HTML_CHARS, function (m) { + return _strReplace(s, SPECIAL_HTML_CHARS, function (m) { return m === '&' ? '&' : m === '<' ? '<' : m === '>' ? '>' @@ -229,7 +270,7 @@ exports._getPrivFilters = function () { // This filter is meant to introduce double-encoding, and should be used with extra care. ya: function(s) { - return strReplace(s, AMP, '&'); + return _strReplace(s, AMP, '&'); }, // FOR DETAILS, refer to inHTMLData() diff --git a/tests/unit/private-xss-filters.js b/tests/unit/private-xss-filters.js index 1b8a656..dc26ef4 100644 --- a/tests/unit/private-xss-filters.js +++ b/tests/unit/private-xss-filters.js @@ -450,7 +450,23 @@ Authors: Nera Liu it('htmlDecode d test', function() { expect(filter.d(null)).to.equal('null'); expect(filter.d()).to.equal('undefined'); - expect(filter.d('Á� €‚”Ÿ�﷐')).to.equal('Á\uFFFD\uFFFD\u20AC\u201A\u201D\u0178\uFFFD\uFFFD'); + expect(filter.d('>a >A Á� �﷐')).to.equal('>a >A Á\uFFFD\uFFFD\uFFFD\uFFFD'); + + var i, specialChar = [ + /*\x80*/ '\u20AC', '\uFFFD', '\u201A', '\u0192', + /*\x84*/ '\u201E', '\u2026', '\u2020', '\u2021', + /*\x88*/ '\u02C6', '\u2030', '\u0160', '\u2039', + /*\x8C*/ '\u0152', '\uFFFD', '\u017D', '\uFFFD', + /*\x90*/ '\uFFFD', '\u2018', '\u2019', '\u201C', + /*\x94*/ '\u201D', '\u2022', '\u2013', '\u2014', + /*\x98*/ '\u02DC', '\u2122', '\u0161', '\u203A', + /*\x9C*/ '\u0153', '\uFFFD', '\u017E', '\u0178', + ]; + for (i = 0x80; i <= 0x9F; i++) { + // console.log(i, '&#x' + i.toString(16) + ';', filter.d('&#x' + i.toString(16) + ';'), specialChar[i - 0x80]) + expect(filter.d('&#x' + i.toString(16) + ';')).to.equal(specialChar[i - 0x80]); + } + }); it('frCoPt exists', function() { @@ -461,9 +477,48 @@ Authors: Nera Liu expect(filter.frCoPt()).to.equal(''); expect(filter.frCoPt(0)).to.equal('\uFFFD'); expect(filter.frCoPt(10)).to.equal('\n'); + expect(filter.frCoPt(0x61)).to.equal('a'); + expect(filter.frCoPt(0x1F600)).to.equal('😀'); expect(filter.frCoPt(0x0B)).to.equal('\uFFFD'); expect(filter.frCoPt(0x10FFFF)).to.equal('\uFFFD'); }); + + it('config exists', function(){ + expect(filter.config).to.be.ok(); + expect(filter.config()).not.to.be.ok(); + }); + it('config test - replaceNull', function() { + + var s = "foo&<>\"'` bar&<>\"' \x00\0<"; + + var beforeConfigResult = [ + filter.yd(s), + filter.yc(s), + filter.yavu(s), + filter.yavd(s), + filter.yavs(s) + ]; + + filter.config({replaceNull:true}); + expect(filter.yd()).to.eql('undefined'); + expect(filter.yd(null)).to.eql('null'); + + expect(filter.yd(s)).to.equal("foo&<>\"'` bar&<>\"' \uFFFD\uFFFD<"); + expect(filter.yc(s)).to.equal("foo&<>\"'` bar&<>\"' \uFFFD\uFFFD<"); + expect(filter.yavu(s)).to.equal("foo&<>"'` bar&<>"' \uFFFD\uFFFD<"); + expect(filter.yavd(s)).to.equal("foo&<>"'` bar&<>"' \uFFFD\uFFFD<"); + expect(filter.yavs(s)).to.equal("foo&<>\"'` bar&<>\"' \uFFFD\uFFFD<"); + + + filter.config({replaceNull:false}); + expect(filter.yd(s)).to.equal("foo&<>\"'` bar&<>\"' \x00\0<"); + expect(filter.yd(s)).to.equal(beforeConfigResult[0]); + expect(filter.yc(s)).to.equal(beforeConfigResult[1]); + expect(filter.yavu(s)).to.equal(beforeConfigResult[2]); + expect(filter.yavd(s)).to.equal(beforeConfigResult[3]); + expect(filter.yavs(s)).to.equal(beforeConfigResult[4]); + + }); }); }()); diff --git a/tests/unit/xss-filters.js b/tests/unit/xss-filters.js index 2957f7b..4d6a861 100644 --- a/tests/unit/xss-filters.js +++ b/tests/unit/xss-filters.js @@ -15,7 +15,7 @@ Authors: Nera Liu var filter = require('../../src/xss-filters'); var testutils = require('../utils.js'); - delete filter._privFilters; + // delete filter._privFilters; delete filter._getPrivFilters; describe("xss-filters: existence tests", function() { @@ -151,8 +151,10 @@ Authors: Nera Liu describe("xss-filters: error tests", function() { it('filters handling of undefined input', function() { - for (var f in filter) - expect(filter[f]()).to.eql('undefined'); + for (var f in filter) { + if (f !== '_privFilters') + expect(filter[f]()).to.eql('undefined'); + } }); }); @@ -451,7 +453,45 @@ Authors: Nera Liu '%60%60', '%20%60%60', '%09%60%60', '%0A%60%60', '%0C%60%60']); testutils.test_yuc(filter.uriFragmentInUnQuotedAttr); }); - }); + + + describe("xss-filters: utility tests", function() { + + it('config exists', function(){ + expect(filter._privFilters.config).to.be.ok(); + }); + it('config test - replaceNull', function() { + + var s = "foo&<>\"'` bar&<>\"' \x00\0<"; + + var beforeConfigResult = [ + filter.inHTMLData(s), + filter.inHTMLComment(s), + filter.inUnQuotedAttr(s), + filter.inDoubleQuotedAttr(s), + filter.inSingleQuotedAttr(s) + ]; + + filter._privFilters.config({replaceNull:true}); + expect(filter.inHTMLData(s)).to.equal("foo&<>\"'` bar&<>\"' \uFFFD\uFFFD<"); + expect(filter.inHTMLComment(s)).to.equal("foo&<>\"'` bar&<>\"' \uFFFD\uFFFD<"); + expect(filter.inUnQuotedAttr(s)).to.equal("foo&<>"'` bar&<>"' \uFFFD\uFFFD<"); + expect(filter.inDoubleQuotedAttr(s)).to.equal("foo&<>"'` bar&<>"' \uFFFD\uFFFD<"); + expect(filter.inSingleQuotedAttr(s)).to.equal("foo&<>\"'` bar&<>\"' \uFFFD\uFFFD<"); + + + filter._privFilters.config({replaceNull:false}); + expect(filter.inHTMLData(s)).to.equal("foo&<>\"'` bar&<>\"' \x00\0<"); + expect(filter.inHTMLData(s)).to.equal(beforeConfigResult[0]); + expect(filter.inHTMLComment(s)).to.equal(beforeConfigResult[1]); + expect(filter.inUnQuotedAttr(s)).to.equal(beforeConfigResult[2]); + expect(filter.inDoubleQuotedAttr(s)).to.equal(beforeConfigResult[3]); + expect(filter.inSingleQuotedAttr(s)).to.equal(beforeConfigResult[4]); + + }); + }); + + }());