Skip to content
This repository was archived by the owner on Jul 15, 2019. It is now read-only.
Open
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
- `<div>` `{{{inHTMLData data}}}` `</div>`
- `<!--` `{{{inHTMLComment comment}}}` `-->`
- `<input value='` `{{{inSingleQuotedAttr value}}}` `'/>`
Expand Down
57 changes: 48 additions & 9 deletions src/xss-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ exports._getPrivFilters = function () {
// By CSS: (Tab|NewLine|colon|semi|lpar|rpar|apos|sol|comma|excl|ast|midast);|(quot|QUOT)
// By URI_PROTOCOL: (Tab|NewLine);
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: '"'};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why only lt etc. is case sensitive? how about other pattern like Lt etc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to http://dev.w3.org/html5/html-author/charref, Lt is not a valid charref. Did you find it accepted by any browsers?
those decoding of & < > " are actually security non-critical, as no regexp is trying to match them. they're there only for those who're interested in using the html decoder (i.e., _privFilters.d()).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as per an offline discussion. adding more comments to explain this list.


// var CSS_VALID_VALUE =
// /^(?:
Expand Down Expand Up @@ -61,15 +61,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);
}
Expand All @@ -80,6 +82,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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we never know the exact implementation of document.write in different browsers, it does not look good to override any DOM object and its api.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this polyfill targets only those IE7-9, where string after a NULL char will be skipped. other browsers are not affected. It basically wraps the original document.write(), so its original unknown implementation is kept intact. Before passing over to the original function, the only thing it does here is just replace NULL with \uFFFD. Therefore, all chars will be consumed for printing. Let's further discuss when we meet. :)

/*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);
Expand Down Expand Up @@ -157,7 +182,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'
Expand All @@ -182,6 +207,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 ? '' :
Expand Down Expand Up @@ -217,7 +256,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 === '&' ? '&amp;'
: m === '<' ? '&lt;'
: m === '>' ? '&gt;'
Expand All @@ -229,7 +268,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, '&amp;');
return _strReplace(s, AMP, '&amp;');
},

// FOR DETAILS, refer to inHTMLData()
Expand Down
57 changes: 56 additions & 1 deletion tests/unit/private-xss-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,23 @@ Authors: Nera Liu <neraliu@yahoo-inc.com>
it('htmlDecode d test', function() {
expect(filter.d(null)).to.equal('null');
expect(filter.d()).to.equal('undefined');
expect(filter.d('&Aacute;&#0;&#x0D;&#x80;&#x82;&#x94;&#x9F;&#xD800;&#xFDD0;')).to.equal('&Aacute;\uFFFD\uFFFD\u20AC\u201A\u201D\u0178\uFFFD\uFFFD');
expect(filter.d('&gta &GTA &Aacute;&#0;&#x0D;&#xD800;&#xFDD0;')).to.equal('>a >A &Aacute;\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() {
Expand All @@ -461,9 +477,48 @@ Authors: Nera Liu <neraliu@yahoo-inc.com>
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&lt;";

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&&lt;>\"'` bar&&lt;>\"' \uFFFD\uFFFD&lt;");
expect(filter.yc(s)).to.equal("foo&<>\"'` bar&<>\"' \uFFFD\uFFFD&lt;");
expect(filter.yavu(s)).to.equal("foo&&lt;&gt;&quot;&#39;&#96;&#32;bar&&lt;&gt;&quot;&#39;&#32;\uFFFD\uFFFD&lt;");
expect(filter.yavd(s)).to.equal("foo&<>&quot;'` bar&<>&quot;' \uFFFD\uFFFD&lt;");
expect(filter.yavs(s)).to.equal("foo&<>\"&#39;` bar&<>\"&#39; \uFFFD\uFFFD&lt;");


filter.config({replaceNull:false});
expect(filter.yd(s)).to.equal("foo&&lt;>\"'` bar&&lt;>\"' \x00\0&lt;");
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]);

});
});

}());
48 changes: 44 additions & 4 deletions tests/unit/xss-filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Authors: Nera Liu <neraliu@yahoo-inc.com>
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() {
Expand Down Expand Up @@ -151,8 +151,10 @@ Authors: Nera Liu <neraliu@yahoo-inc.com>
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');
}
});
});

Expand Down Expand Up @@ -451,7 +453,45 @@ Authors: Nera Liu <neraliu@yahoo-inc.com>
'%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&lt;";

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&&lt;>\"'` bar&&lt;>\"' \uFFFD\uFFFD&lt;");
expect(filter.inHTMLComment(s)).to.equal("foo&<>\"'` bar&<>\"' \uFFFD\uFFFD&lt;");
expect(filter.inUnQuotedAttr(s)).to.equal("foo&&lt;&gt;&quot;&#39;&#96;&#32;bar&&lt;&gt;&quot;&#39;&#32;\uFFFD\uFFFD&lt;");
expect(filter.inDoubleQuotedAttr(s)).to.equal("foo&<>&quot;'` bar&<>&quot;' \uFFFD\uFFFD&lt;");
expect(filter.inSingleQuotedAttr(s)).to.equal("foo&<>\"&#39;` bar&<>\"&#39; \uFFFD\uFFFD&lt;");


filter._privFilters.config({replaceNull:false});
expect(filter.inHTMLData(s)).to.equal("foo&&lt;>\"'` bar&&lt;>\"' \x00\0&lt;");
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]);

});
});


}());