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, '' + i.toString(16) + ';', filter.d('' + i.toString(16) + ';'), specialChar[i - 0x80])
+ expect(filter.d('' + 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]);
+
+ });
+ });
+
+
}());