diff --git a/Gruntfile.js b/Gruntfile.js index 84c7824..68c688d 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -29,7 +29,7 @@ module.exports = function(grunt) { }, browserify: { standalone: { - src: [ 'src/<%= pkg.name %>.js' ], + src: [ 'src/index-node.js' ], dest: 'dist/<%= pkg.name %>.js', options: { browserifyOptions: { @@ -48,23 +48,61 @@ module.exports = function(grunt) { join_vars: true } }, - buildBrowserified: { - src: 'dist/<%= pkg.name %>.js', - dest: 'dist/<%= pkg.name %>.min-browserified.js' + // buildBrowserified: { + // src: 'dist/<%= pkg.name %>.js', + // dest: 'dist/<%= pkg.name %>.min-browserified.js' + // }, + buildUrlOnly: { + options: { + wrap: 'urlFilters' + }, + src: [ + 'src/lib/hostParser.js', + 'src/lib/urlFilters.js', + 'src/lib/urlResolver.js'], + dest: 'dist/url-filters-only.min.<%= pkg.version %>.js' + }, + buildXssOnly: { + options: { + wrap: 'xssFilters' + }, + src: [ + 'src/index-browser.js', + 'src/lib/htmlDecode.js', + 'src/lib/xssFilters.priv.js', + 'src/lib/xssFilters.js' + ], + dest: 'dist/xss-filters.min.<%= pkg.version %>.js' }, buildMin: { options: { wrap: 'xssFilters' }, - src: 'src/<%= pkg.name %>.js', - dest: 'dist/<%= pkg.name %>.min.js' + src: [ + 'src/index-browser.js', + 'src/lib/hostParser.js', + 'src/lib/urlFilters.js', + 'src/lib/urlResolver.js', + 'src/lib/htmlDecode.js', + 'src/lib/xssFilters.priv.js', + 'src/lib/xssFilters.js' + ], + dest: 'dist/all-filters.min.js' }, buildMinWithVersion: { options: { wrap: 'xssFilters' }, - src: 'src/<%= pkg.name %>.js', - dest: 'dist/<%= pkg.name %>.<%= pkg.version %>.min.js' + src: [ + 'src/index-browser.js', + 'src/lib/hostParser.js', + 'src/lib/urlFilters.js', + 'src/lib/urlResolver.js', + 'src/lib/htmlDecode.js', + 'src/lib/xssFilters.priv.js', + 'src/lib/xssFilters.js' + ], + dest: 'dist/all-filters.min.<%= pkg.version %>.js' } }, mocha_istanbul: { @@ -98,7 +136,7 @@ module.exports = function(grunt) { }); grunt.loadNpmTasks('grunt-mocha-istanbul'); - grunt.loadNpmTasks('grunt-browserify'); + // grunt.loadNpmTasks('grunt-browserify'); grunt.loadNpmTasks('grunt-contrib-uglify'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-clean'); @@ -112,7 +150,8 @@ module.exports = function(grunt) { testSet.push('dist', 'karma:ci'); grunt.registerTask('test', testSet); - grunt.registerTask('dist', ['browserify', 'uglify']) + // grunt.registerTask('dist', ['browserify', 'uglify']) + grunt.registerTask('dist', 'uglify') grunt.registerTask('docs', ['jsdoc']); grunt.registerTask('default', ['test', 'dist']); diff --git a/dist/xss-filters.min-browserified.js b/dist/xss-filters.min-browserified.js deleted file mode 100644 index 531419d..0000000 --- a/dist/xss-filters.min-browserified.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * xss-filters - v1.2.6 - * Yahoo! Inc. Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms. - */ -!function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.xssFilters=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g=55296&&57343>=c||13===c?"�":f.frCoPt(c)):b[e||g]||a}return b=b||p,c=c||o,void 0===a?"undefined":null===a?"null":a.toString().replace(k,"�").replace(c,e)}function c(a){return"\\"+a.charCodeAt(0).toString(16).toLowerCase()+" "}function d(a){return a.replace(t,function(a){return"-x-"+a})}function e(c){c=f.yufull(b(c));var d=a(c);return d&&w[d.toLowerCase()]?"##"+c:c}var f,g=/])/g,m=/[&<>"'`]/g,n=/(?:\x00|^-*!?>|--!?>|--?!?$|\]>|\]$)/g,o=/&(?:#([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,p={Tab:" ",NewLine:"\n",colon:":",semi:";",lpar:"(",rpar:")",apos:"'",sol:"/",comma:",",excl:"!",ast:"*",midast:"*",ensp:" ",emsp:" ",thinsp:" ",nbsp:" ",amp:"&",lt:"<",gt:">",quot:'"',QUOT:'"'},q=/^(?:(?!-*expression)#?[-\w]+|[+-]?(?:\d+|\d*\.\d+)(?:r?em|ex|ch|cm|mm|in|px|pt|pc|%|vh|vw|vmin|vmax)?|!important|)$/i,r=/[\x00-\x1F\x7F\[\]{}\\"]/g,s=/[\x00-\x1F\x7F\[\]{}\\']/g,t=/url[\(\u207D\u208D]+/g,u=/['\(\)]/g,v=/\/\/%5[Bb]([A-Fa-f0-9:]+)%5[Dd]/,w={javascript:1,data:1,vbscript:1,mhtml:1,"x-schema":1},x=/(?::|&#[xX]0*3[aA];?|�*58;?|:)/,y=/(?:^[\x00-\x20]+|[\t\n\r\x00]+)/g,z={Tab:" ",NewLine:"\n"},A=function(a,b,c){return void 0===a?"undefined":null===a?"null":a.toString().replace(b,c)},B=String.fromCodePoint||function(a){return 0===arguments.length?"":65535>=a?String.fromCharCode(a):(a-=65536,String.fromCharCode((a>>10)+55296,a%1024+56320))};return f={frCoPt:function(a){return void 0===a||null===a?"":!isFinite(a=Number(a))||0>=a||a>1114111||a>=1&&8>=a||a>=14&&31>=a||a>=127&&159>=a||a>=64976&&65007>=a||11===a||65535===(65535&a)||65534===(65535&a)?"�":B(a)},d:b,yup:function(c){return c=a(c.replace(k,"")),c?b(c,z,null,!0).replace(y,"").toLowerCase():null},y:function(a){return A(a,m,function(a){return"&"===a?"&":"<"===a?"<":">"===a?">":'"'===a?""":"'"===a?"'":"`"})},ya:function(a){return A(a,j,"&")},yd:function(a){return A(a,g,"<")},yc:function(a){return A(a,n,function(a){return"\x00"===a?"�":"--!"===a||"--"===a||"-"===a||"]"===a?a+" ":a.slice(0,-1)+" >"})},yavd:function(a){return A(a,h,""")},yavs:function(a){return A(a,i,"'")},yavu:function(a){return A(a,l,function(a){return" "===a?" ":"\n"===a?" ":" "===a?" ":"\f"===a?" ":"\r"===a?" ":" "===a?" ":"="===a?"=":"<"===a?"<":">"===a?">":'"'===a?""":"'"===a?"'":"`"===a?"`":"�"})},yu:encodeURI,yuc:encodeURIComponent,yubl:function(a){return w[f.yup(a)]?"x-"+a:a},yufull:function(a){return f.yu(a).replace(v,function(a,b){return"//["+b+"]"})},yublf:function(a){return f.yubl(f.yufull(a))},yceu:function(a){return a=b(a),q.test(a)?a:";-x:'"+d(a.replace(s,c))+"';-v:"},yced:function(a){return d(b(a).replace(r,c))},yces:function(a){return d(b(a).replace(s,c))},yceuu:function(a){return e(a).replace(u,function(a){return"'"===a?"\\27 ":"("===a?"%28":"%29"})},yceud:function(a){return e(a)},yceus:function(a){return e(a).replace(i,"\\27 ")}}};var e=c._privFilters=c._getPrivFilters();c.inHTMLData=e.yd,c.inHTMLComment=e.yc,c.inSingleQuotedAttr=e.yavs,c.inDoubleQuotedAttr=e.yavd,c.inUnQuotedAttr=e.yavu,c.uriInSingleQuotedAttr=function(a){return d(a,e.yavs)},c.uriInDoubleQuotedAttr=function(a){return d(a,e.yavd)},c.uriInUnQuotedAttr=function(a){return d(a,e.yavu)},c.uriInHTMLData=e.yufull,c.uriInHTMLComment=function(a){return e.yc(e.yufull(a))},c.uriPathInSingleQuotedAttr=function(a){return d(a,e.yavs,e.yu)},c.uriPathInDoubleQuotedAttr=function(a){return d(a,e.yavd,e.yu)},c.uriPathInUnQuotedAttr=function(a){return d(a,e.yavu,e.yu)},c.uriPathInHTMLData=e.yu,c.uriPathInHTMLComment=function(a){return e.yc(e.yu(a))},c.uriQueryInSingleQuotedAttr=c.uriPathInSingleQuotedAttr,c.uriQueryInDoubleQuotedAttr=c.uriPathInDoubleQuotedAttr,c.uriQueryInUnQuotedAttr=c.uriPathInUnQuotedAttr,c.uriQueryInHTMLData=c.uriPathInHTMLData,c.uriQueryInHTMLComment=c.uriPathInHTMLComment,c.uriComponentInSingleQuotedAttr=function(a){return e.yavs(e.yuc(a))},c.uriComponentInDoubleQuotedAttr=function(a){return e.yavd(e.yuc(a))},c.uriComponentInUnQuotedAttr=function(a){return e.yavu(e.yuc(a))},c.uriComponentInHTMLData=e.yuc,c.uriComponentInHTMLComment=function(a){return e.yc(e.yuc(a))},c.uriFragmentInSingleQuotedAttr=function(a){return e.yubl(e.yavs(e.yuc(a)))},c.uriFragmentInDoubleQuotedAttr=function(a){return e.yubl(e.yavd(e.yuc(a)))},c.uriFragmentInUnQuotedAttr=function(a){return e.yubl(e.yavu(e.yuc(a)))},c.uriFragmentInHTMLData=c.uriComponentInHTMLData,c.uriFragmentInHTMLComment=c.uriComponentInHTMLComment},{}]},{},[1])(1)}); \ No newline at end of file diff --git a/karma.conf.js b/karma.conf.js index 694e072..0c6377f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -2,7 +2,7 @@ module.exports = function(config) { if (!process.env.SAUCE_USERNAME || !process.env.SAUCE_ACCESS_KEY) { console.warn('No SAUCE credentials found (missing SAUCE_USERNAME and SAUCE_ACCESS_KEY env variables). Skipping SauceLabs testing.'); - return; + // return; } // Browsers to run on Sauce Labs @@ -103,7 +103,7 @@ module.exports = function(config) { // list of files / patterns to load in the browser files: [ 'node_modules/expect.js/index.js', - 'dist/xss-filters.min.js', + 'dist/all-filters.min.js', 'tests/polyfills.js', 'tests/utils.js', 'tests/unit/*.js' diff --git a/package.json b/package.json index a4893f1..547cd8a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xss-filters", - "version": "1.2.6", + "version": "2.0.0", "licenses": [ { "type": "BSD", @@ -23,7 +23,7 @@ "email": "albertyu@yahoo-inc.com" } ], - "main": "src/xss-filters.js", + "main": "src/index-node.js", "scripts": { "test": "grunt test", "hint": "grunt jshint", @@ -46,8 +46,8 @@ ], "devDependencies": { "expect.js": "^0.3.1", + "extend": "^3.0.0", "grunt": "^0.4.5", - "grunt-browserify": "^3.8.0", "grunt-cli": "^0.1.13", "grunt-contrib-clean": "^0.6.0", "grunt-contrib-copy": "^0.7.0", diff --git a/src/index-browser.js b/src/index-browser.js new file mode 100644 index 0000000..57a3309 --- /dev/null +++ b/src/index-browser.js @@ -0,0 +1,17 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Adonis Fung + Nera Liu + Albert Yu +*/ + +// This file is used only for building the min version with uglify +/*jshint esnext: true *//*jshint -W079 *//*jshint unused:false*/ +const require = false; + +exports._privFilters = { + urlFilters: (exports.urlFilters = {}) +}; \ No newline at end of file diff --git a/src/index-node.js b/src/index-node.js new file mode 100644 index 0000000..e9dfa2a --- /dev/null +++ b/src/index-node.js @@ -0,0 +1,38 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ +/** + * The following file serves the node.js version + */ + +/*jshint node: true */ +var extend = require('extend'); + +// populate the xss filters +module.exports = exports = require('./lib/xssFilters'); + +// add yHtmlDecode to _privFilters +extend(exports._privFilters, require('./lib/htmlDecode')); + +// the following is largely designed for secure-handlebars-helpers +exports._getPrivFilters = { + toString : function() { + var fs = require('fs'); + return '(function(){var xssFilters={},exports={};' + + fs.readFileSync('./src/lib/htmlDecode.js', 'utf8') + + fs.readFileSync('./src/lib/xssFilters.priv.js', 'utf8') + + 'return exports;})'; + } +}; + +// add urlFilters +exports.urlFilters = extend( + require('./lib/urlFilters'), + require('./lib/hostParser'), + require('./lib/urlResolver')); \ No newline at end of file diff --git a/src/lib/hostParser.js b/src/lib/hostParser.js new file mode 100644 index 0000000..d59c9e6 --- /dev/null +++ b/src/lib/hostParser.js @@ -0,0 +1,180 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ + +var _urlFilters = exports.urlFilters || exports; + +// designed according to https://url.spec.whatwg.org/#percent-decode +var _reHostInvalidSyntax = /[\x00\t\n\r#%\/:?@\[\\\]]/g, + _reUpto4HexDigits = /^[\dA-Fa-f]{1,4}$/, + _yUrl256Power = [1, 256, 65536, 16777216, 4294967296], + _reValidNumber = /^(?:0[xX][\dA-Fa-f]*|0[0-7]*|(?!0)\d*)$/; + +function _yIPv4NumberParser(part) { + if (!_reValidNumber.test(part)) return NaN; + + var n, len = part.length; + return (len > 1 && part.slice(0, 2).toLowerCase() === '0x') ? (len === 2 ? 0 : parseInt(part.slice(2), 16)) : + (len > 1 && part.charCodeAt(0) === 48) ? parseInt(part, 8) : + parseInt(part); +} + + +_urlFilters.hostParser = function (input, options) { + /*jshint -W030 */ + options || (options = {}); + + var FAILURE = null, lastIdx; + + // the order of percent decoding is swapped with ipv6 to follow browser behavior + try { + // Let domain be the result of utf-8 decode without BOM on the percent + // decoding of utf-8 encode on input. + input = decodeURIComponent(input); + } catch(e) { return FAILURE; } + + lastIdx = input.length - 1; + + // trigger ipv6 parser + // _reUrlAuthHostsPort has excluded those not ending with ] + if (input.charCodeAt(0) === 91) { // begins with [ + return input.charCodeAt(lastIdx) === 93 ? // ends with ] + _urlFilters.ipv6Parser(input.slice(1, lastIdx)) : // && valid ipv6 + FAILURE; + } + + // Let asciiDomain be the result of running domain to ASCII on domain. + // If asciiDomain is failure, return failure. + options.IDNAtoASCII && (input = punycode.toASCII(input)); + + // If asciiDomain contains one of U+0000, U+0009, U+000A, U+000D, U+0020, + // "#", "%", "/", ":", "?", "@", "[", "\", and "]", syntax violation, + // return failure. + // We follow this except the space character U+0020 + if (_reHostInvalidSyntax.test(input)) { return FAILURE; } + + return _urlFilters.ipv4Parser(input); +} + + +_urlFilters.ipv4Parser = function (input) { + var chunks = input.split('.'), + len = chunks.length, + i = 0, + FAILURE = null, INPUT = {'domain': 1, 'host': input.toLowerCase()}, + outputA = '', outputB = ''; + + // If the last item in parts is the empty string, remove the last item from + // parts + chunks[len - 1].length === 0 && len--; + + // If parts has more than four items, return input + if (len > 4) { return INPUT; } + + // parse the number in every item. + for (; i < len; i++) { + // If part is the empty string, return input. + // 0..0x300 is a domain, not an IPv4 address. + if (chunks[i].length === 0 || + // If n is failure, return input. + isNaN((chunks[i] = _yIPv4NumberParser(chunks[i])))) { return INPUT; } + } + + // If any but the last item in numbers > 255, return failure + for (i = 0; i < len - 1; i++) { + if ((n = chunks[i]) > 255) { return FAILURE; } + // Use them directly as output + outputA += n + '.'; + } + + // If the last item in numbers is greater than or equal to 256^(5 − the + // number of items in numbers), syntax violation, return failure. + if ((n = chunks[i]) >= _yUrl256Power[5 - len]) { return FAILURE; } + + // IPv4 serializer composes anything after outputA + for (i = len - 1; i < 4; i++) { + // Prepend n % 256, serialized, to output. + outputB = (n % 256) + outputB; + // Unless this is the fourth time, prepend "." to output. + (i !== 3) && (outputB = '.' + outputB); + // Set n to n / 256. + n = Math.floor(n / 256); + } + + // Return output as {'ipv4': 1, 'host': 'IPv4_ADDRESS'} + return {'ipv4': 1, 'host': outputA + outputB}; +} + +_urlFilters.ipv6Parser = function(input) { + if (input === '::') { return {'ipv6': 1, 'host': '[::]'}; } + + var chunks = input.split(':'), + FAILURE = null, compressPtr = null, compressAtEdge = false, + i = 0, len = chunks.length, piece, result; + + // too little or many colons than allowed + if (len < 3 || len > 9 || + // start with a single colon (except double-colon) + chunks[0].length === 0 && chunks[1].length !== 0) { return FAILURE; } + + // capture as many 4-hex-digits as possible + for (; i < len; i++) { + piece = chunks[i]; + // empty indicates a colon is found + if (piece.length === 0) { + // 2nd colon found + if (compressPtr !== null) { + // double-colon allowed once, and only at either start or end + if (!compressAtEdge && (i === 1 || compressPtr === len - 2)) { + compressAtEdge = true; + continue; + } + return FAILURE; + } + // if the input ends with a single colon + if (i === len - 1) { return FAILURE; } + // point to the first colon position + compressPtr = i; + } + // check if the piece conform to 4 hex digits + else if (_reUpto4HexDigits.test(piece)) { + // lowercased, and leading zeros are removed + chunks[i] = parseInt(piece, 16).toString(16); + } + // quit the loop once a piece is found not matching 4 hex digits + else { break; } + } + + // all pieces conform to the 4-hex pattern + if (i === len) {} + // only the last one doesn't conform to 4 hex digits + // has at most 6 4-hex pieces (7 is due to the leading compression) + // it must has a dot to trigger ipv4 parsing (excluded number only) + // it's a valid IPv4 address + else if (i === len - 1 && + i < (compressAtEdge ? 8 : 7) && + piece.indexOf('.') !== -1 && + (result = _urlFilters.ipv4Parser(piece)) && result.ipv4) { + // replace the last piece with two pieces of ipv4 in hexadecimal + result = result.host.split('.'); + chunks[i] = (result[0] * 0x100 + parseInt(result[1])).toString(16); + chunks[len++] = (result[2] * 0x100 + parseInt(result[3])).toString(16); + } + else { return FAILURE; } + + // insert zero for ipv6 that has 7 chunks plus a compressor + if (compressAtEdge) { + --len === 8 && chunks.splice(compressPtr, 2, '0'); + } else if (len === 8 && compressPtr !== null) { + chunks[compressPtr] = '0'; + } + + // return the input in string if there're less than 8 chunks + return len === 9 ? FAILURE : {'ipv6': 1, 'host': '[' + chunks.join(':') + ']'}; +} diff --git a/src/lib/htmlDecode.js b/src/lib/htmlDecode.js new file mode 100644 index 0000000..82c7814 --- /dev/null +++ b/src/lib/htmlDecode.js @@ -0,0 +1,199 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ + +var x = exports._privFilters || exports; + +// Specified here are those html entities selected for decoding, they're sensitive to the following contexts +// CSS: (Tab|NewLine|colon|semi|lpar|rpar|apos|sol|comma|excl|ast|midast|commat|lbrace|lcub|rbrace|rcub);|(quot|QUOT);? // CSS sensitive chars: ()"'/,!*@:;{} +// URI: (bsol|commat|num|sol|quest|percnt|Tab|NewLine); // URL sensitive chars: \@#/?%\t\n +// common: (amp|AMP|gt|GT|lt|LT|quote|QUOT);? +// To generate trie, given {'Tab;':'\t', 'NewLine;': '\n' ... }, (no semi-colon needed if it's optional) +// replace it repeatedly using /'(\w)([\w;]+)'\s*:\s*'([^']*)'/g with '$1': {'$2': '$3'} +/*jshint -W075 */ +var trieNamedRef = { + 'A': {'M': {'P': '&'}}, + 'G': {'T': '>'}, + 'L': {'T': '<'}, + 'N': {'e': {'w': {'L': {'i': {'n': {'e': {';': '\n'}}}}}}}, + 'Q': {'U': {'O': {'T': '"'}}}, + 'T': {'a': {'b': {';': '\t'}}}, + 'a': {'m': {'p': '&'}, + 'p': {'o': {'s': {';': '\''}}}, + 's': {'t': {';': '*'}}}, + 'b': {'s': {'o': {'l': {';': '\\'}}}}, + 'c': {'o': {'m': {'m': {'a': {';': ',', + 't': {';': '@'}}}}, + 'l': {'o': {'n': {';': ':'}}}}}, + 'e': {'x': {'c': {'l': {';': '!'}}}, + 'n': {'s': {'p': {';': '\u2002'}}}, + 'm': {'s': {'p': {';': '\u2003'}}}}, + 'g': {'t': '>'}, + 'l': {'b': {'r': {'a': {'c': {'e': {';': '{'}}}}}, + 'c': {'u': {'b': {';': '{'}}}, + 'p': {'a': {'r': {';': '('}}}, + 't': '<'}, + 'm': {'i': {'d': {'a': {'s': {'t': {';': '*'}}}}}}, + 'n': {'b': {'s': {'p': '\xA0'}}, + 'u': {'m': {';': '#'}}}, + 'p': {'e': {'r': {'c': {'n': {'t': {';': '%'}}}}}}, + 'q': {'u': {'o': {'t': '"'}, + 'e': {'s': {'t': {';': '?'}}}}}, + 'r': {'b': {'r': {'a': {'c': {'e': {';': '}'}}}}}, + 'c': {'u': {'b': {';': '}'}}}, + 'p': {'a': {'r': {';': ')'}}}}, + 's': {'e': {'m': {'i': {';': ';'}}}, + 'o': {'l': {';': '/'}}}, + 't': {'h': {'i': {'n': {'s': {'p': {';': '\u2009'}}}}}} +}; +// Ref: https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference +var specialCharToken = [ + /*\x80*/ '\u20AC', '\u0081', '\u201A', '\u0192', + /*\x84*/ '\u201E', '\u2026', '\u2020', '\u2021', + /*\x88*/ '\u02C6', '\u2030', '\u0160', '\u2039', + /*\x8C*/ '\u0152', '\u008D', '\u017D', '\u008F', + /*\x90*/ '\u0090', '\u2018', '\u2019', '\u201C', + /*\x94*/ '\u201D', '\u2022', '\u2013', '\u2014', + /*\x98*/ '\u02DC', '\u2122', '\u0161', '\u203A', + /*\x9C*/ '\u0153', '\u009D', '\u017E', '\u0178' +]; +var fromCodePoint = String.fromCodePoint ? String.fromCodePoint : function(codePoint) { + return String.fromCharCode(( (codePoint -= 0x10000) >> 10) + 0xD800, (codePoint % 0x400) + 0xDC00); +}; +// the spec defines some invalid chars, +// - only some of those require \uFFFD replacement +// - for others, return the corresponding character +function getHtmlDecodedChar(codePoint) { + return (codePoint >= 0x80 && codePoint <= 0x9F) ? specialCharToken[codePoint - 0x80] + : (codePoint >= 0xD800 && codePoint <= 0xDFFF) || codePoint === 0 || codePoint > 0x10FFFF ? '\uFFFD' + : codePoint <= 0xFFFF ? String.fromCharCode(codePoint) // BMP code point + // Astral code point; split in surrogate halves + // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae + : fromCodePoint(codePoint); +} +/* + * @param {string} s - An untrusted user input + * @param {boolean} skipNullReplacement - set to true only if string s has NO NULL characters (e.g., string is once html-decoded, or input stream pre-processing has taken place). Caution: When set, given &\x00#39;, it is not decoded, and as a result, IE can still dangerously decode it as '. Leave it unset to result in &\uFFFD#39;, that won't be decoded further by browsers) + * + * @returns {string} The html decoded string (All numbered entities will be decoded. See trieNamedRef in source code for a supported list of named entities) + */ +x.yHtmlDecode = function(s, skipNullReplacement) { + if (s === undefined || s === null) { return ''; } + s = s.toString(); + + var c, lastIdx = 0, ampIdx = -1, bufIdx = -1, subTrie, output = '', state = 0, i = 0, len = s.length; + + while (i < len) { + c = s.charCodeAt(i); + + if (state === 0) { // init state + + if (c === 0x26) { // & collected, switch to state 1 + state = 1; + ampIdx = i; + } + + } else if (state === 1) { // & previously collected + + if (c === 0x23) { // # collected, switch to number matching (state 2) + state = 2; + // } else if (c >= 0x41 && c <= 0x5A || + // c >= 0x61 && c <= 0x7A || + // c >= 0x30 && c <= 0x39) { // alphanumeric collected, switch to named ref matching (state 6) + } else if ((subTrie = trieNamedRef[s[i]])) { + state = 5; + // bufIdx = i; // since subTrie is used to track + } else { // otherwise, not a character reference. + state = -1; + } + + } else if (state === 2) { // # previously collected + if (c === 0x78 || c === 0x58) { // X or x collected, process as hex (state 16) + state = 16; + } else if (c >= 0x30 && c <= 0x39) { // digits collected, process as dec (state 10) + state = 10; + bufIdx = i; + } else { + state = -1; + } + + } else if (state === 16) { // xX previously collected (hex) + + if (c >= 0x30 && c <= 0x39 || + c >= 0x41 && c <= 0x46 || + c >= 0x61 && c <= 0x66) { // A-Fa-f0-9 collected + if (bufIdx === -1) { + bufIdx = i; + } + } else { + if (bufIdx > 0) { // non-digits char encountered + output += s.slice(lastIdx, ampIdx) + getHtmlDecodedChar(parseInt(s.slice(bufIdx, i), state)); + // consume one more char if the current one is semicolon + lastIdx = c === 0x3B ? i + 1 : i; + } + state = -1; + } + + } else if (state === 10) { // 0-9 previously collected (dec) + if (c >= 0x30 && c <= 0x39) { + // bufIdx is set before entering state 10 + } else if (bufIdx > 0) { + output += s.slice(lastIdx, ampIdx) + getHtmlDecodedChar(parseInt(s.slice(bufIdx, i))); + lastIdx = c === 0x3B ? i + 1 : i; + state = -1; + } + } else if (state === 5) { // named ref collected + + // assumed named ref must have at least 2 chars + // if ((c >= 0x41 && c <= 0x5A || + // c >= 0x61 && c <= 0x7A) && + // c >= 0x30 && c <= 0x39 // here's an optimization as no char ref contains number + if ((subTrie = subTrie[s[i]])) { + if (typeof subTrie === 'string') { + output += s.slice(lastIdx, ampIdx) + subTrie; + // when the last hit char is not semicolon, consume the next semicolon, if any + if (c !== 0x3B && s.charCodeAt(i+1) === 0x3B) { i++; } + lastIdx = i + 1; + state = -1; + } + } else { + state = -1; + } + } + + // reset to init state + if (state === -1) { + + // reconsume the & char as if in state 0, when it's used to terminate an entity (e.g., '') + if (c === 0x26) { + state = 1; + ampIdx = i; + bufIdx = -1; + } else { + state = 0; + ampIdx = bufIdx = -1; + } + } + + // replace Null with \uFFFD + if (!skipNullReplacement && c === 0) { + output += s.slice(lastIdx, i) + '\uFFFD'; + lastIdx = i + 1; + } + + i++; + } + + // flash any collected numbered references + if ((state === 16 || state === 10) && bufIdx > 0) { + return output + s.slice(lastIdx, ampIdx) + getHtmlDecodedChar(parseInt(s.slice(bufIdx, i), state)); + } + + return lastIdx === 0 ? s : output + s.slice(lastIdx); +} \ No newline at end of file diff --git a/src/lib/urlFilters.js b/src/lib/urlFilters.js new file mode 100644 index 0000000..5f861c7 --- /dev/null +++ b/src/lib/urlFilters.js @@ -0,0 +1,330 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ + +var _urlFilters = exports.urlFilters || exports; + +// In general, hostname must be present, auth/port optional +// ^[\/\\]* is to ignore any number of leading slashes. +// Spec says at most 2 slashes after scheme, otherwise syntax violation +// here, we follow browsers' behavior to ignore syntax violation +// Ref: https://url.spec.whatwg.org/#special-authority-ignore-slashes-state +// (?:([^\/\\?#]*)@)? captures the authority (user:pass) without the trailing @ +// We omitted here any encoding/separation for username and password +// Ref: https://url.spec.whatwg.org/#authority-state +// (IPV6-LIKE|VALID_HOST) is explained as follows: +// [^\x00#\/:?\[\]\\]+ captures chars that can specify domain and IPv4 +// - @ won't appear here anyway as it's captured by prior regexp +// - \x20 is found okay to browser, so it's also allowed here +// - \t\n\r is allowed here, to be later stripped (follows browser behavior) +// (?:\[|%5[bB])(?:[^\/?#\\]+)\] is to capture ipv6-like address +// - \t\n\r is allowed here, to be later stripped (follows browser behavior) +// Ref: https://url.spec.whatwg.org/#concept-ipv6-parser +// Ref: https://url.spec.whatwg.org/#concept-host-parser +// (?:ENDS_W/OPTIONAL_COLON|OPTIONAL_PORT|FOLLOWED_BY_DELIMETER) explanations: +// :?$ tests if it ends with an optional colon +// (?::([\\d\\t\\n\\r]*))? captures the port number if any +// whitespaces to be later stripped +// Ref: https://url.spec.whatwg.org/#port-state +// (?=[\/?#\\]) means a pathname follows (delimeter is not captured though) +// this is required to ensure the whole hostname is matched +// Ref: https://url.spec.whatwg.org/#host-state +var _reUrlAuthHostsPort = /^[\/\\]*(?:([^\/\\?#]*)@)?((?:\[|%5[bB])(?:[^\x00\/?#\\]+)\]|[^\x00#\/:?\[\]\\]+)(?::?$|:([\d\t\n\r]+)|(?=[\/?#\\]))/; + +// Schemes including file, gopher, ws and wss are not heavily tested +// https://url.spec.whatwg.org/#special-scheme +_urlFilters.specialSchemeDefaultPort = {'ftp:': '21', 'file:': '', 'gopher:': '70', 'http:': '80', 'https:': '443', 'ws:': '80', 'wss:': '443'}; + +/** + * The prototype of urlFilterFactoryAbsCallback + * + * @callback urlFilterFactoryAbsCallback + * @param {string} url + * @param {string} scheme - relative scheme is indicated as ''. no trailing colon. always lowercased + * @param {string} authority - no trailing @ if exists. username & password both included. no percent-decoding + * @param {string} hostname - no percent-decoding. always lowercased + * @param {string} port - non-default port number. no leading colon. no percent-decoding + * @returns the url, or anything of your choice + */ + +/** + * The prototype of urlFilterFactoryRelCallback + * + * @callback urlFilterFactoryRelCallback + * @param {string} path + * @returns the url, or anything of your choice + */ + +/** + * The prototype of urlFilterFactoryUnsafeCallback + * + * @callback urlFilterFactoryUnsafeCallback + * @param {string} url + * @returns the url, or anything of your choice (e.g., 'unsafe:' + url) + */ + +/* + * This creates a URL whitelist filter, which largely observes + * the specification in https://url.spec.whatwg.org/#url-parsing. It is + * designed for matching whitelists of schemes and hosts, and will thus + * parse only up to a sufficient position (i.e., faster for not requiring + * to parse the whole URL). + * + * It simplifies the spec: base URL is null, utf-8 encoding, no state + * override, no hostname parsing, no percent-decoding, no username and + * password parsing within the authority + * It adds to the spec: aligned w/browsers to accept \t\n\r within origin + * + * @param {Object} options allow configurations as follows + * @param {Object[]} options.schemes - an optional array of schemes + * (trailing colon optional). If not provided, only http and https are + * allowed + * @param {boolean} options.relScheme - to enable relative scheme (//) + * @param {Object[]} options.hostnames - an optional array of hostnames that + * each matches /^[\w\.-]+$/. If any one is found unmatched, return null + * @param {boolean} options.subdomain - to enable subdomain matching for + * non-IPs specified in options.hostnames + * @param {boolean} options.parseHost - the specified options.hostnames + * are validated against URLs after their hosts are parsed and normalized, + * as specified in https://url.spec.whatwg.org/#host-parsing. Even if + * options.hostnames are not provided, absCallback are called only if a host + * can be parsed correctly, otherwise, call unsafeCallback instead. + * @param {boolean} options.relPath - to allow relative path + * @param {boolean} options.relPathOnly - to allow relative path only + * @param {boolean} options.imgDataURIs - to allow data scheme with the + * MIME type equal to image/gif, image/jpeg, image/jpg, or image/png, and + * the encoding format as base64 + * @param {boolean} options.IDNAtoASCII - convert all domains to its ASCII + * format according to RFC 3492 and RFC 5891 for matching/comparisons. See + * https://nodejs.org/api/punycode.html for details. + * @param {urlFilterFactoryAbsCallback} options.absCallback - if matched, + * called to further process the url, scheme, hostname, non-default port, and + * path + * @param {urlFilterFactoryRelCallback} options.relCallback - if matched, + * called to further process the path + * @param {urlFilterFactoryUnsafeCallback} options.unsafeCallback - called + * to further process any unmatched url. if not provided, the default is + * to prefix those unmatched url with "unsafe:" + * @returns {function} The returned function taking (url) runs the + * configured tests. It prefixes "unsafe:" to non-matching URLs, and + * handover to the options.absCallback and/or options.relCallback for + * matched ones, and options.unsafeCallback for unmatched ones. In case + * no callback is provided, return the matched url or prefix it with + * "unsafe:" for unmatched ones. + */ +_urlFilters.create = function (options) { + /*jshint -W030 */ + options || (options = {}); + + function _parseHostAbsCallback (url, scheme, auth, host, port, path) { + var i = 0, re, t, hostnames = options.hostnames, + safeCallback = function (t) { + return _safeAbsCallback(url, scheme, auth, t.host, port, path, t); + }; + // parse the host + if ((t = _urlFilters.hostParser(host, options)) !== null) { + // no hostnames enforcement + if (!hostnames) { + return safeCallback(t); + } + // if subdomain enabled, use regexp to check if a host is whitelisted + else if (options.subdomain) { + for (; re = reHosts[i]; i++) { + if (re.test ? re.test(t.host) : re === t.host) { + return safeCallback(t); + } + } + } + // host found in options.hostnames + else if (hostnames.indexOf(t.host) !== -1) { + return safeCallback(t); + } + } + return unsafeCallback(url); + } + + var i, n, arr, t, reElement, reProtos, reAuthHostsPort, reHosts = [], + _safeCallback = function(url) { return url; }, + _safeAbsCallback = options.absCallback || _safeCallback, + absCallback = options.parseHost ? _parseHostAbsCallback : _safeAbsCallback, + relCallback = options.relCallback || _safeCallback, + unsafeCallback = options.unsafeCallback || function(url) { return 'unsafe:' + url; }, + // reEscape escapes chars that are sensitive to regexp + reEscape = /[.*?+\\\[\](){}|\^$]/g, + // the following whitespaces are allowed in origin + reOriginWhitespaces = /[\t\n\r]+/g, + // reImgDataURIs hardcodes the image data URIs that are known to be safe + reImgDataURIs = options.imgDataURIs && /^(data):image\/(?:jpe?g|gif|png);base64,[a-z0-9+\/=]*$/i, + // reRelPath ensures the URL has no scheme/auth/hostname/port + // (?![a-z][a-z0-9+-.\t\n\r]*:) avoided going to the #scheme-state + // \t\n\r can be part of scheme according to browsers' behavior + // (?![\/\\]{2}) avoided the transitions from #relative-state, + // to #relative-slash-state and then to + // #special-authority-ignore-slashes-state + // Ref: https://url.spec.whatwg.org/ + reRelPath = (options.relPath || options.relPathOnly) && + /^(?![a-z][a-z0-9+-.\t\n\r]*:|[\/\\]{2})/i; + + // build reProtos if options.schemes are provided + // in any case, reProtos won't match a relative path + if ((arr = options.schemes) && (n = arr.length)) { + // reElement specifies the possible chars for scheme + // Ref: https://url.spec.whatwg.org/#scheme-state + reElement = /^([a-z][a-z0-9+-.]*):?$/i; + + for (i = 0; i < n; i++) { + if ((t = reElement.exec(arr[i]))) { + // lowercased the scheme with the trailing colon skipped + t = t[1].toLowerCase(); + } else { + // throw TypeError if an array element cannot be validated + throw new TypeError(t + ' is an invalid scheme.'); + } + // escapes t from regexp sensitive chars + arr[i] = t.replace(reEscape, '\\$&'); + } + + // build reProtos from the schemes array, must be case insensitive + // The relScheme matching regarding [\/\\]{2} summarized the transitions from + // #relative-state, #relative-slash-state to #special-authority-ignore-slashes-state + // Ref: https://url.spec.whatwg.org/ + reProtos = new RegExp( + '^(?:((?:' + + arr.join('|') + + (options.relScheme ? '):)|[\\/\\\\]{2})' : '):))'), 'i'); + + } else { + // the default reProtos, only http and https are allowed. + // refer to above for regexp explanations + reProtos = options.relScheme ? /^(?:(https?:)|[\/\\]{2})/i : /^(https?:)/i; + } + + // clean reHosts first + reHosts.length = 0; + + // build reAuthHostsPort if options.hostnames are provided + if ((arr = options.hostnames) && (n = arr.length)) { + for (i = 0; i < n; i++) { + // throw TypeError if an array element cannot be validated + if ((t = _urlFilters.hostParser(arr[i], options)) === null) { + throw new TypeError(arr[i] + ' is an invalid hostname.'); + } + + // relax for subdomain for those domain elements + reHosts[i] = (options.subdomain && t.domain) ? + // See above for valid hostname requirement + // accept \t\n\r, which will be later stripped + '(?:[^\\x00#\\/:?\\[\\]\\\\]+\\.)*' : + ''; + + // escapes t from regexp sensitive chars + reHosts[i] += (arr[i] = t.host).replace(reEscape, '\\$&'); + + if (options.parseHost && options.subdomain) { + reHosts[i] = t.domain ? new RegExp('^' + reHosts[i] + '$') : t.host; + } + } + + // build reAuthHostsPort from the hosts array, must be case insensitive + // it is based on _reUrlAuthHostsPort, see comments there for details + // The difference here is to directly match the hostnames using regexp: + // '(' + arr.join('|') + ')' is to match the whitelisted hostnames + reAuthHostsPort = options.parseHost ? _reUrlAuthHostsPort : + new RegExp('^[\\/\\\\]*(?:([^\\/\\\\?#]*)@)?' + + '(' + reHosts.join('|') + ')' + // allowed hostnames, in regexp + '(?::?$|:([\\d\\t\\n\\r]+)|(?=[\\/?#\\\\]))', 'i'); + } else { + // options.subdomain must be false given no options.hostnames + delete options.hostnames; + + // extract the auth, hostname and port number if options.absCallback is supplied + if (options.absCallback) { + // use the default reAuthHostsPort. see _reUrlAuthHostsPort for details + reAuthHostsPort = _reUrlAuthHostsPort; + } + } + + /* + * @param {string} url + * @returns {string|} the url - the url itself, or prefixed with + * "unsafe:" if it fails the tests. In case absCallback/relCallback + * is supplied, the output is controled by the callback for those + * urls that pass the tests. + */ + return function(url) { + var scheme, authHostPort, i = 0, charCode, remainingUrl, defaultPort, port, empty = ''; + + // handle special types + if (url === undefined || typeof url === 'object') { + url = empty; + } else { + url = url.toString(); + + // remove leading whitespaces (don't care the trailing whitespaces) + // Ref: #1 in https://url.spec.whatwg.org/#concept-basic-url-parser + while ((charCode = url.charCodeAt(i)) >= 0 && charCode <= 32) { i++; } + i > 0 && (url = url.slice(i)); + } + + // options.relPathOnly will bypass any check on scheme + if (options.relPathOnly) { + return reRelPath.test(url) ? relCallback(url) : unsafeCallback(url); + } + + // reRelPath ensures no scheme/auth/hostname/port + if (options.relPath && reRelPath.test(url)) { + return relCallback(url); + } + + // match the scheme, could be from a safe image Data URI + if ((scheme = reProtos.exec(url) || + reImgDataURIs && reImgDataURIs.exec(url))) { + + // get the remaining url for further matching + remainingUrl = url.slice(scheme[0].length); + + // !reAuthHostsPort means no restrictions on auth/host/port, implied + // no options.absCallback is present + if (!reAuthHostsPort) { return url; } + + // scheme[1] could be empty when relScheme is set. When it holds + // a whitelisted scheme, no reOriginWhitespaces treatment as + // applied to auth/hostname/port is needed due to the regexp used + // specialSchemeDefaultPort[scheme[1].toLowerCase()] gets the + // default port number of those special scheme. It's undefined if + // it's a non-special scheme. + // So, here non-special scheme, just consider + // anything beyond scheme as pathname + if (scheme[1]) { + scheme[1] = scheme[1].toLowerCase(); + defaultPort = _urlFilters.specialSchemeDefaultPort[scheme[1]]; + if (defaultPort === undefined) { + return absCallback(url, scheme[1], empty, empty, empty, remainingUrl); + } + } else { + scheme[1] = empty; + } + + // if auth, hostname and port are properly validated + if ((authHostPort = reAuthHostsPort.exec(remainingUrl))) { + // strip \t\r\n in origin like browsers to handle syntax violations + port = authHostPort[3] ? authHostPort[3].replace(reOriginWhitespaces, empty) : empty; // port + return absCallback(url, + scheme[1], + authHostPort[1] ? authHostPort[1].replace(reOriginWhitespaces, empty) : empty, // auth + authHostPort[2].replace(reOriginWhitespaces, empty), // host + port === defaultPort ? empty : port, // pass '' instead of the default port, if given + remainingUrl.slice(authHostPort[0].length)); + } + } + + return unsafeCallback(url); + }; +}; + diff --git a/src/lib/urlResolver.js b/src/lib/urlResolver.js new file mode 100644 index 0000000..8093fa7 --- /dev/null +++ b/src/lib/urlResolver.js @@ -0,0 +1,159 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ +var _urlFilters = exports.urlFilters || exports; + +// for node js version +if (typeof require === 'function') { + _urlFilters.create = require('./urlFilters').create; + _urlFilters.specialSchemeDefaultPort = require('./urlFilters').specialSchemeDefaultPort; +} + +function _composeOriginSchemePath(scheme, auth, hostname, port, path) { + var specialScheme = scheme === '' || _urlFilters.specialSchemeDefaultPort[scheme] !== undefined, + origin = specialScheme ? scheme + '//' : scheme, + c; + + auth && (origin += auth + '@'); + origin += hostname; + port && (origin += ':' + port); + + if (specialScheme && (path.length === 0 || + (c = path.charCodeAt(0)) !== 47 && c !== 92)) { // char / or \ + path = '/' + path; + } + + return [origin, scheme, path]; +} + +function _absUrlResolver(url, origin, scheme, path, baseOrigin, baseScheme, basePath, options) { + return (scheme === '' ? baseScheme : '') + origin + path; +} + +var _rePathDoubleDots = /^(?:\.|%2[eE]){2}$/, + _rePathSingleDot = /^(?:\.|%2[eE])$/, + _rePathQueryOrFragment = /[?#]/, + _rePathLastFile = /(?:[\/\\](?!(?:\.|%2[eE]){2})[^\/\\?#]*)?(?:$|[?#])/; + +// return 1 for slash, 2 for ?/#, 0 otherwise +function _resolvePathSymbol(path, i) { + var c = path.charCodeAt(i); + return c === 47 || c === 92 ? 1 : c === 35 || c === 63 ? 2 : 0; +} + +// This follows the spec except those specific for the file scheme +// Ref: https://url.spec.whatwg.org/#path-state +function _resolvePath(path, scheme) { + // _composeOriginSchemePath() normalized path to have at least the first / + var i = 1, j = 1, len = path.length, arrPathLen = 0, symbol, + arrPath = [], buffer, slash = /*scheme === 'file:' ? '\\' :*/ '/'; + while (j <= len) { + if (j === len /* EOF */ || (symbol = _resolvePathSymbol(path, j))) { + buffer = path.slice(i, j); + + if (_rePathDoubleDots.test(buffer)) { + arrPathLen !== 0 && --arrPathLen; + symbol !== 1 && (arrPath[arrPathLen++] = ''); + } else if (_rePathSingleDot.test(buffer)) { + symbol !== 1 && (arrPath[arrPathLen++] = ''); + } else { + arrPath[arrPathLen++] = buffer; + } + + // supposedly switch to query or fragment state, which is dont care here + if (symbol === 2) { break; } + // pos index of character that is just after the last slash + i = j + 1; + } + j++; + } + // aggregate the path as string + the remaining query/fragment + return slash + arrPath.slice(0, arrPathLen).join(slash) + path.slice(j); +} + +function _relUrlResolver(path, baseOrigin, baseScheme, basePath, options) { + var pos = -1, t, resolve = options.resolvePath ? _resolvePath : function(p) {return p;}; + + if (path.length === 0) { + return baseOrigin + resolve(basePath, baseScheme); + } + + switch (path.charCodeAt(0)) { + case 47: case 92: /* / or \ */ + return baseOrigin + resolve(path, baseScheme); + case 35: /* # */ + if (!options.appendFragment) { return path; } // no _resolvePath needed + pos = basePath.indexOf('#'); + break; + case 63: /* ? */ + (t = _rePathQueryOrFragment.exec(basePath)) && (pos = t.index); + break; + default: + (t = _rePathLastFile.exec(basePath)) && (pos = t.index); + path = '/' + path; + } + + // replace base path's component, if any, with the new one + return baseOrigin + resolve( + (pos === -1 ? basePath : basePath.slice(0, pos)) + path, baseScheme); +} + +function _unsafeUrlResolver(url) { + return 'unsafe:' + url; +} + +_urlFilters.yUrlResolver = function (options) { + options || (options = {}); + + var bFilter, urlFilter, _baseURL, + schemes = options.schemes, + relScheme = options.relScheme !== false, + absSchemeResolver = options.absResolver || {}, + relSchemeResolver = options.relResolver || {}, + absResolver = typeof options.absResolver === 'function' ? options.absResolver : _absUrlResolver, + relResolver = typeof options.relResolver === 'function' ? options.relResolver : _relUrlResolver, + unsafeResolver = options.unsafeResolver || _unsafeUrlResolver; + + options.resolvePath = options.resolvePath !== false; + + function initUrlFilter() { + urlFilter = _urlFilters.create({ + schemes: schemes, + relScheme: relScheme, + relPath: true, + absCallback: function(url, scheme, auth, hostname, port, path) { + var args = _composeOriginSchemePath(scheme || '', auth, hostname, port, path); + return (absSchemeResolver[_baseURL[1]] || absResolver)( + url, args[0], args[1], args[2], _baseURL[0], _baseURL[1], _baseURL[2], options); + }, + relCallback: function(path) { + return (relSchemeResolver[_baseURL[1]] || relResolver)( + path, _baseURL[0], _baseURL[1], _baseURL[2], options); + }, + unsafeCallback: unsafeResolver + }); + } + + bFilter = _urlFilters.create({ + relScheme: relScheme, + schemes: schemes, + absCallback: function(url, scheme, auth, hostname, port, path) { + _baseURL = _composeOriginSchemePath(scheme || '', auth, hostname, port, path); + !urlFilter && initUrlFilter(); + return true; + }, + unsafeCallback: function() { return false; } + }); + + return function(url, baseURL) { + return (arguments.length >= 2 && !bFilter(baseURL) ? + unsafeResolver : + urlFilter || unsafeResolver)(url); + }; +}; diff --git a/src/xss-filters.js b/src/lib/xssFilters.js similarity index 56% rename from src/xss-filters.js rename to src/lib/xssFilters.js index 42994eb..cbd8f5e 100644 --- a/src/xss-filters.js +++ b/src/lib/xssFilters.js @@ -7,470 +7,19 @@ Authors: Nera Liu Adonis Fung Albert Yu */ -/*jshint node: true */ - -exports._getPrivFilters = function () { - - var LT = /])/g, - SPECIAL_HTML_CHARS = /[&<>"'`]/g, - SPECIAL_COMMENT_CHARS = /(?:\x00|^-*!?>|--!?>|--?!?$|\]>|\]$)/g; - - // var CSS_VALID_VALUE = - // /^(?: - // (?!-*expression)#?[-\w]+ - // |[+-]?(?:\d+|\d*\.\d+)(?:em|ex|ch|rem|px|mm|cm|in|pt|pc|%|vh|vw|vmin|vmax)? - // |!important - // | //empty - // )$/i; - var CSS_VALID_VALUE = /^(?:(?!-*expression)#?[-\w]+|[+-]?(?:\d+|\d*\.\d+)(?:r?em|ex|ch|cm|mm|in|px|pt|pc|%|vh|vw|vmin|vmax)?|!important|)$/i, - // TODO: prevent double css escaping by not encoding \ again, but this may require CSS decoding - // \x7F and \x01-\x1F less \x09 are for Safari 5.0, added []{}/* for unbalanced quote - CSS_DOUBLE_QUOTED_CHARS = /[\x00-\x1F\x7F\[\]{}\\"]/g, - CSS_SINGLE_QUOTED_CHARS = /[\x00-\x1F\x7F\[\]{}\\']/g, - // (, \u207D and \u208D can be used in background: 'url(...)' in IE, assumed all \ chars are encoded by QUOTED_CHARS, and null is already replaced with \uFFFD - // otherwise, use this CSS_BLACKLIST instead (enhance it with url matching): /(?:\\?\(|[\u207D\u208D]|\\0{0,4}28 ?|\\0{0,2}20[78][Dd] ?)+/g - CSS_BLACKLIST = /url[\(\u207D\u208D]+/g, - // this assumes encodeURI() and encodeURIComponent() has escaped 1-32, 127 for IE8 - CSS_UNQUOTED_URL = /['\(\)]/g; // " \ treated by encodeURI() - - // Given a full URI, need to support "[" ( IPv6address ) "]" in URI as per RFC3986 - // Reference: https://tools.ietf.org/html/rfc3986 - var URL_IPV6 = /\/\/%5[Bb]([A-Fa-f0-9:]+)%5[Dd]/; - - - // Reference: http://shazzer.co.uk/database/All/characters-allowd-in-html-entities - // Reference: http://shazzer.co.uk/vector/Characters-allowed-after-ampersand-in-named-character-references - // Reference: http://shazzer.co.uk/database/All/Characters-before-javascript-uri - // Reference: http://shazzer.co.uk/database/All/Characters-after-javascript-uri - // Reference: https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference - // Reference for named characters: https://html.spec.whatwg.org/multipage/entities.json - var URI_BLACKLIST_PROTOCOLS = {'javascript':1, 'data':1, 'vbscript':1, 'mhtml':1, 'x-schema':1}, - URI_PROTOCOL_WHITESPACES = /(?:^[\x00-\x20]+|[\t\n\r\x00]+)/g, - URI_PROTOCOL_ENCODED = /^([\x00-\x20&#a-zA-Z0-9;+-.]*:?)/; - - var x, - strReplace = function (s, regexp, callback) { - return s === undefined ? 'undefined' - : s === null ? 'null' - : s.toString().replace(regexp, callback); - }; - - - // Specified here are those html entities selected for decoding, they're sensitive to the following contexts - // CSS: (Tab|NewLine|colon|semi|lpar|rpar|apos|sol|comma|excl|ast|midast);|(quot|QUOT);? // CSS sensitive chars: ()"'/,!*@{}:; - // URI: (bsol|Tab|NewLine); - // common: (amp|AMP|gt|GT|lt|LT|quote|QUOT);? - // To generate trie, given {'Tab;':'\t', 'NewLine;': '\n' ... }, (no semi-colon needed if it's optional) - // replace it repeatedly using /'(\w)([\w;]+)'\s*:\s*'([^']*)'/g with '$1': {'$2': '$3'} - /*jshint -W075 */ - var trieNamedRef = { - 'A': {'M': {'P': '&'}}, - 'G': {'T': '>'}, - 'L': {'T': '<'}, - 'N': {'e': {'w': {'L': {'i': {'n': {'e': {';': '\n'}}}}}}}, - 'Q': {'U': {'O': {'T': '"'}}}, - 'T': {'a': {'b': {';': '\t'}}}, - 'a': {'m': {'p': '&'}, 'p': {'o': {'s': {';': '\''}}}, 's': {'t': {';': '*'}}}, - 'b': {'s': {'o': {'l': {';': '\\'}}}}, - 'c': {'o': {'m': {'m': {'a': {';': ','}}}}, 'o': {'l': {'o': {'n': {';': ':'}}}}}, - 'e': {'x': {'c': {'l': {';': '!'}}}, 'n': {'s': {'p': {';': '\u2002'}}}, 'm': {'s': {'p': {';': '\u2003'}}}}, - 'g': {'t': '>'}, - 'l': {'p': {'a': {'r': {';': '('}}}, 't': '<'}, - 'm': {'i': {'d': {'a': {'s': {'t': {';': '*'}}}}}}, - 'n': {'b': {'s': {'p': '\xA0'}}}, - 'q': {'u': {'o': {'t': '"'}}}, - 'r': {'p': {'a': {'r': {';': ')'}}}}, - 's': {'e': {'m': {'i': {';': ';'}}}, 'o': {'l': {';': '/'}}}, - 't': {'h': {'i': {'n': {'s': {'p': {';': '\u2009'}}}}}} - - }; - // Ref: https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference - var specialCharToken = [ - /*\x80*/ '\u20AC', '\u0081', '\u201A', '\u0192', - /*\x84*/ '\u201E', '\u2026', '\u2020', '\u2021', - /*\x88*/ '\u02C6', '\u2030', '\u0160', '\u2039', - /*\x8C*/ '\u0152', '\u008D', '\u017D', '\u008F', - /*\x90*/ '\u0090', '\u2018', '\u2019', '\u201C', - /*\x94*/ '\u201D', '\u2022', '\u2013', '\u2014', - /*\x98*/ '\u02DC', '\u2122', '\u0161', '\u203A', - /*\x9C*/ '\u0153', '\u009D', '\u017E', '\u0178' - ]; - var fromCodePoint = String.fromCodePoint ? String.fromCodePoint : function(codePoint) { - return String.fromCharCode(( (codePoint -= 0x10000) >> 10) + 0xD800, (codePoint % 0x400) + 0xDC00); - }; - // the spec defines some invalid chars, - // - only some of those require \uFFFD replacement - // - for others, return the corresponding character - function getHtmlDecodedChar(codePoint) { - return (codePoint >= 0x80 && codePoint <= 0x9F) ? specialCharToken[codePoint - 0x80] - : (codePoint >= 0xD800 && codePoint <= 0xDFFF) || codePoint === 0 || codePoint > 0x10FFFF ? '\uFFFD' - : codePoint <= 0xFFFF ? String.fromCharCode(codePoint) // BMP code point - // Astral code point; split in surrogate halves - // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae - : fromCodePoint(codePoint); - } - /* - * @param {string} s - An untrusted user input - * @param {boolean} skipNullReplacement - set to true only if string s has NO NULL characters (e.g., string is once html-decoded, or input stream pre-processing has taken place). Caution: When set, given &\x00#39;, it is not decoded, and as a result, IE can still dangerously decode it as '. Leave it unset to result in &\uFFFD#39;, that won't be decoded further by browsers) - * - * @returns {string} The html decoded string (All numbered entities will be decoded. See trieNamedRef in source code for a supported list of named entities) - */ - function htmlDecode(s, skipNullReplacement) { - if (s === undefined || s === null) { return ''; } - s = s.toString(); - - var c, lastIdx = 0, ampIdx = -1, bufIdx = -1, subTrie, output = '', state = 0, i = 0, len = s.length; - - while (i < len) { - c = s.charCodeAt(i); - - if (state === 0) { // init state - - if (c === 0x26) { // & collected, switch to state 1 - state = 1; - ampIdx = i; - } - - } else if (state === 1) { // & previously collected - - if (c === 0x23) { // # collected, switch to number matching (state 2) - state = 2; - // } else if (c >= 0x41 && c <= 0x5A || - // c >= 0x61 && c <= 0x7A || - // c >= 0x30 && c <= 0x39) { // alphanumeric collected, switch to named ref matching (state 6) - } else if ((subTrie = trieNamedRef[s[i]])) { - state = 5; - // bufIdx = i; // since subTrie is used to track - } else { // otherwise, not a character reference. - state = -1; - } - - } else if (state === 2) { // # previously collected - if (c === 0x78 || c === 0x58) { // X or x collected, process as hex (state 16) - state = 16; - } else if (c >= 0x30 && c <= 0x39) { // digits collected, process as dec (state 10) - state = 10; - bufIdx = i; - } else { - state = -1; - } - - } else if (state === 16) { // xX previously collected (hex) - - if (c >= 0x30 && c <= 0x39 || - c >= 0x41 && c <= 0x46 || - c >= 0x61 && c <= 0x66) { // A-Fa-f0-9 collected - if (bufIdx === -1) { - bufIdx = i; - } - } else { - if (bufIdx > 0) { // non-digits char encountered - output += s.slice(lastIdx, ampIdx) + getHtmlDecodedChar(parseInt(s.slice(bufIdx, i), state)); - // consume one more char if the current one is semicolon - lastIdx = c === 0x3B ? i + 1 : i; - } - state = -1; - } - - } else if (state === 10) { // 0-9 previously collected (dec) - if (c >= 0x30 && c <= 0x39) { - // bufIdx is set before entering state 10 - } else if (bufIdx > 0) { - output += s.slice(lastIdx, ampIdx) + getHtmlDecodedChar(parseInt(s.slice(bufIdx, i))); - lastIdx = c === 0x3B ? i + 1 : i; - state = -1; - } - } else if (state === 5) { // named ref collected - - // assumed named ref must have at least 2 chars - // if ((c >= 0x41 && c <= 0x5A || - // c >= 0x61 && c <= 0x7A) && - // c >= 0x30 && c <= 0x39 // here's an optimization as no char ref contains number - if ((subTrie = subTrie[s[i]])) { - if (typeof subTrie === 'string') { - output += s.slice(lastIdx, ampIdx) + subTrie; - // when the last hit char is not semicolon, consume the next semicolon, if any - if (c !== 0x3B && s.charCodeAt(i+1) === 0x3B) { i++; } - lastIdx = i + 1; - state = -1; - } - } else { - state = -1; - } - } - - // reset to init state - if (state === -1) { - - // reconsume the & char as if in state 0, when it's used to terminate an entity (e.g., '') - if (c === 0x26) { - state = 1; - ampIdx = i; - bufIdx = -1; - } else { - state = 0; - ampIdx = bufIdx = -1; - } - } - - // replace Null with \uFFFD - if (!skipNullReplacement && c === 0) { - output += s.slice(lastIdx, i) + '\uFFFD'; - lastIdx = i + 1; - } - - i++; - } - - // flash any collected numbered references - if ((state === 16 || state === 10) && bufIdx > 0) { - return output + s.slice(lastIdx, ampIdx) + getHtmlDecodedChar(parseInt(s.slice(bufIdx, i), state)); - } - - return lastIdx === 0 ? s : output + s.slice(lastIdx); - } - - function getProtocol(str, skipHtmlDecoding) { - var m = skipHtmlDecoding || str.match(URI_PROTOCOL_ENCODED), i; - if (m) { - if (!skipHtmlDecoding) { - // getProtocol() must run a greedy html decode algorithm, i.e., omit all NULLs before decoding (as in IE) - // hence, \x00javascript:, &\x00#20;javascript:, and java\x00script: can all return javascript - // and since all NULL is replaced with '', we can set skipNullReplacement in htmlDecode() - str = htmlDecode(m[1].replace(NULL, ''), true); - } - i = str.indexOf(':'); - if (i !== -1) { - return str.slice(0, i).replace(URI_PROTOCOL_WHITESPACES, '').toLowerCase(); - } - } - return null; - } - - - function cssEncode(chr) { - // space after \\HEX is needed by spec - return '\\' + chr.charCodeAt(0).toString(16).toLowerCase() + ' '; - } - function cssBlacklist(s) { - return s.replace(CSS_BLACKLIST, function(m){ return '-x-' + m; }); - } - function cssUrl(s) { - return s === undefined ? 'undefined' - : s === null ? 'null' - // encodeURI() in yufull() will throw error for use of the CSS_UNSUPPORTED_CODE_POINT (i.e., [\uD800-\uDFFF]) - // prefix ## for blacklisted protocols - // it's safe to skipHtmlDecoding for getProtocol() as s is already html-decoded (and also all NULLs are replaced with \uFFFD) - : URI_BLACKLIST_PROTOCOLS[getProtocol((s = x.yufull(htmlDecode(s.toString()))), true)] ? '##' + s : s; - } - - return (x = { - yHtmlDecode: htmlDecode, - /* - * @param {string} s - An untrusted uri input - * @returns {string} s - null if no protocol found, otherwise the protocol with whitespaces stripped and lower-cased - */ - yup: getProtocol, - - /* - * @deprecated - * @param {string} s - An untrusted user input - * @returns {string} s - The original user input with & < > " ' ` encoded respectively as & < > " ' and `. - * - */ - y: function(s) { - return strReplace(s, SPECIAL_HTML_CHARS, function (m) { - return m === '&' ? '&' - : m === '<' ? '<' - : m === '>' ? '>' - : m === '"' ? '"' - : m === "'" ? ''' - : /*m === '`'*/ '`'; // in hex: 60 - }); - }, - // This filter is meant to introduce double-encoding, and should be used with extra care. - ya: function(s) { - return strReplace(s, AMP, '&'); - }, - - // FOR DETAILS, refer to inHTMLData() - // Reference: https://html.spec.whatwg.org/multipage/syntax.html#data-state - yd: function (s) { - return strReplace(s, LT, '<'); - }, - - // FOR DETAILS, refer to inHTMLComment() - // All NULL characters in s are first replaced with \uFFFD. - // If s contains -->, --!>, or starts with -*>, insert a space right before > to stop state breaking at - // If s ends with --!, --, or -, append a space to stop collaborative state breaking at {{{yc s}}}>, {{{yc s}}}!>, {{{yc s}}}-!>, {{{yc s}}}-> - // Reference: https://html.spec.whatwg.org/multipage/syntax.html#comment-state - // Reference: http://shazzer.co.uk/vector/Characters-that-close-a-HTML-comment-3 - // Reference: http://shazzer.co.uk/vector/Characters-that-close-a-HTML-comment - // Reference: http://shazzer.co.uk/vector/Characters-that-close-a-HTML-comment-0021 - // If s contains ]> or ends with ], append a space after ] is verified in IE to stop IE conditional comments. - // Reference: http://msdn.microsoft.com/en-us/library/ms537512%28v=vs.85%29.aspx - // We do not care --\s>, which can possibly be intepreted as a valid close comment tag in very old browsers (e.g., firefox 3.6), as specified in the html4 spec - // Reference: http://www.w3.org/TR/html401/intro/sgmltut.html#h-3.2.4 - yc: function (s) { - return strReplace(s, SPECIAL_COMMENT_CHARS, function(m){ - return m === '\x00' ? '\uFFFD' - : m === '--!' || m === '--' || m === '-' || m === ']' ? m + ' ' - :/* - : m === ']>' ? '] >' - : m === '-->' ? '-- >' - : m === '--!>' ? '--! >' - : /-*!?>/.test(m) ? */ m.slice(0, -1) + ' >'; - }); - }, - - // FOR DETAILS, refer to inDoubleQuotedAttr() - // Reference: https://html.spec.whatwg.org/multipage/syntax.html#attribute-value-(double-quoted)-state - yavd: function (s) { - return strReplace(s, QUOT, '"'); - }, - - // FOR DETAILS, refer to inSingleQuotedAttr() - // Reference: https://html.spec.whatwg.org/multipage/syntax.html#attribute-value-(single-quoted)-state - yavs: function (s) { - return strReplace(s, SQUOT, '''); - }, - - // FOR DETAILS, refer to inUnQuotedAttr() - // PART A. - // if s contains any state breaking chars (\t, \n, \v, \f, \r, space, and >), - // they are escaped and encoded into their equivalent HTML entity representations. - // Reference: http://shazzer.co.uk/database/All/Characters-which-break-attributes-without-quotes - // Reference: https://html.spec.whatwg.org/multipage/syntax.html#attribute-value-(unquoted)-state - // - // PART B. - // if s starts with ', " or `, encode it resp. as ', ", or ` to - // enforce the attr value (unquoted) state - // Reference: https://html.spec.whatwg.org/multipage/syntax.html#before-attribute-value-state - // Reference: http://shazzer.co.uk/vector/Characters-allowed-attribute-quote - // - // PART C. - // Inject a \uFFFD character if an empty or all null string is encountered in - // unquoted attribute value state. - // - // Rationale 1: our belief is that developers wouldn't expect an - // empty string would result in ' name="passwd"' rendered as - // attribute value, even though this is how HTML5 is specified. - // Rationale 2: an empty or all null string (for IE) can - // effectively alter its immediate subsequent state, we choose - // \uFFFD to end the unquoted attr - // state, which therefore will not mess up later contexts. - // Rationale 3: Since IE 6, it is verified that NULL chars are stripped. - // Reference: https://html.spec.whatwg.org/multipage/syntax.html#attribute-value-(unquoted)-state - // - // Example: - // - yavu: function (s) { - return strReplace(s, SPECIAL_ATTR_VALUE_UNQUOTED_CHARS, function (m) { - return m === '\t' ? ' ' // in hex: 09 - : m === '\n' ? ' ' // in hex: 0A - : m === '\x0B' ? ' ' // in hex: 0B for IE. IE<9 \v equals v, so use \x0B instead - : m === '\f' ? ' ' // in hex: 0C - : m === '\r' ? ' ' // in hex: 0D - : m === ' ' ? ' ' // in hex: 20 - : m === '=' ? '=' // in hex: 3D - : m === '<' ? '<' - : m === '>' ? '>' - : m === '"' ? '"' - : m === "'" ? ''' - : m === '`' ? '`' - : /*empty or null*/ '\uFFFD'; - }); - }, - - yu: encodeURI, - yuc: encodeURIComponent, - - // Notice that yubl MUST BE APPLIED LAST, and will not be used independently (expected output from encodeURI/encodeURIComponent and yavd/yavs/yavu) - // This is used to disable JS execution capabilities by prefixing x- to ^javascript:, ^vbscript: or ^data: that possibly could trigger script execution in URI attribute context - yubl: function (s) { - // here the output s in either branch will not be html-decoded, - return URI_BLACKLIST_PROTOCOLS[getProtocol(s)] ? 'x-' + s : s; - }, - - // This is NOT a security-critical filter. - // Reference: https://tools.ietf.org/html/rfc3986 - yufull: function (s) { - return x.yu(s).replace(URL_IPV6, function(m, p) { - return '//[' + p + ']'; - }); - }, - - // chain yufull() with yubl() - yublf: function (s) { - return x.yubl(x.yufull(s)); - }, - - // The design principle of the CSS filter MUST meet the following goal(s). - // (1) The input cannot break out of the context (expr) and this is to fulfill the just sufficient encoding principle. - // (2) The input cannot introduce CSS parsing error and this is to address the concern of UI redressing. - // - // term - // : unary_operator? - // [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* | - // TIME S* | FREQ S* ] - // | STRING S* | IDENT S* | URI S* | hexcolor | function - // - // Reference: - // * http://www.w3.org/TR/CSS21/grammar.html - // * http://www.w3.org/TR/css-syntax-3/ - // - // NOTE: delimiter in CSS - \ _ : ; ( ) " ' / , % # ! * @ . { } - // 2d 5c 5f 3a 3b 28 29 22 27 2f 2c 25 23 21 2a 40 2e 7b 7d - - yceu: function(s) { - s = htmlDecode(s); - return CSS_VALID_VALUE.test(s) ? s : ";-x:'" + cssBlacklist(s.replace(CSS_SINGLE_QUOTED_CHARS, cssEncode)) + "';-v:"; - }, - - // string1 = \"([^\n\r\f\\"]|\\{nl}|\\[^\n\r\f0-9a-f]|\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?)*\" - yced: function(s) { - return cssBlacklist(htmlDecode(s).replace(CSS_DOUBLE_QUOTED_CHARS, cssEncode)); - }, - - // string2 = \'([^\n\r\f\\']|\\{nl}|\\[^\n\r\f0-9a-f]|\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?)*\' - yces: function(s) { - return cssBlacklist(htmlDecode(s).replace(CSS_SINGLE_QUOTED_CHARS, cssEncode)); - }, - - // for url({{{yceuu url}}} - // unquoted_url = ([!#$%&*-~]|\\{h}{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])* (CSS 2.1 definition) - // unquoted_url = ([^"'()\\ \t\n\r\f\v\u0000\u0008\u000b\u000e-\u001f\u007f]|\\{h}{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])* (CSS 3.0 definition) - // The state machine in CSS 3.0 is more well defined - http://www.w3.org/TR/css-syntax-3/#consume-a-url-token0 - // CSS_UNQUOTED_URL = /['\(\)]/g; // " \ treated by encodeURI() - yceuu: function(s) { - return cssUrl(s).replace(CSS_UNQUOTED_URL, function (chr) { - return chr === '\'' ? '\\27 ' : - chr === '(' ? '%28' : - /* chr === ')' ? */ '%29'; - }); - }, - - // for url("{{{yceud url}}} - yceud: function(s) { - return cssUrl(s); - }, +/** +* Yahoo Secure XSS Filters - just sufficient output filtering to prevent XSS! +* @module xss-filters +*/ - // for url('{{{yceus url}}} - yceus: function(s) { - return cssUrl(s).replace(SQUOT, '\\27 '); - } - }); -}; +/*jshint node: true */ +var _privFilters = exports._privFilters; -// exposing privFilters -// this is an undocumented feature, and please use it with extra care -var privFilters = exports._privFilters = exports._getPrivFilters(); +// for node js version +if (typeof require === 'function') { + _privFilters = exports._privFilters = require('./xssFilters.priv'); +} /* chaining filters */ @@ -480,14 +29,9 @@ var privFilters = exports._privFilters = exports._getPrivFilters(); // Rationale: given pattern like this: // developer may expect s is always prefixed with ? or /, but an attacker can abuse it with 'javascript:alert(1)' function uriInAttr (s, yav, yu) { - return privFilters.yubl(yav((yu || privFilters.yufull)(s))); + return _privFilters.yubl(yav((yu || _privFilters.yufull)(s))); } -/** -* Yahoo Secure XSS Filters - just sufficient output filtering to prevent XSS! -* @module xss-filters -*/ - /** * @function module:xss-filters#inHTMLData * @@ -505,7 +49,7 @@ function uriInAttr (s, yav, yu) { *
{{{inHTMLData htmlData}}}
* */ -exports.inHTMLData = privFilters.yd; +exports.inHTMLData = _privFilters.yd; /** * @function module:xss-filters#inHTMLComment @@ -533,7 +77,7 @@ exports.inHTMLData = privFilters.yd; * * */ -exports.inHTMLComment = privFilters.yc; +exports.inHTMLComment = _privFilters.yc; /** * @function module:xss-filters#inSingleQuotedAttr @@ -555,7 +99,7 @@ exports.inHTMLComment = privFilters.yc; * * */ -exports.inSingleQuotedAttr = privFilters.yavs; +exports.inSingleQuotedAttr = _privFilters.yavs; /** * @function module:xss-filters#inDoubleQuotedAttr @@ -577,7 +121,7 @@ exports.inSingleQuotedAttr = privFilters.yavs; * * */ -exports.inDoubleQuotedAttr = privFilters.yavd; +exports.inDoubleQuotedAttr = _privFilters.yavd; /** * @function module:xss-filters#inUnQuotedAttr @@ -610,7 +154,7 @@ exports.inDoubleQuotedAttr = privFilters.yavd; * * */ -exports.inUnQuotedAttr = privFilters.yavu; +exports.inUnQuotedAttr = _privFilters.yavu; /** @@ -637,7 +181,7 @@ exports.inUnQuotedAttr = privFilters.yavu; * */ exports.uriInSingleQuotedAttr = function (s) { - return uriInAttr(s, privFilters.yavs); + return uriInAttr(s, _privFilters.yavs); }; /** @@ -664,7 +208,7 @@ exports.uriInSingleQuotedAttr = function (s) { * */ exports.uriInDoubleQuotedAttr = function (s) { - return uriInAttr(s, privFilters.yavd); + return uriInAttr(s, _privFilters.yavd); }; @@ -692,7 +236,7 @@ exports.uriInDoubleQuotedAttr = function (s) { * */ exports.uriInUnQuotedAttr = function (s) { - return uriInAttr(s, privFilters.yavu); + return uriInAttr(s, _privFilters.yavu); }; /** @@ -718,7 +262,7 @@ exports.uriInUnQuotedAttr = function (s) { *
{{{uriInHTMLData full_uri}}} * */ -exports.uriInHTMLData = privFilters.yufull; +exports.uriInHTMLData = _privFilters.yufull; /** @@ -745,7 +289,7 @@ exports.uriInHTMLData = privFilters.yufull; * */ exports.uriInHTMLComment = function (s) { - return privFilters.yc(privFilters.yufull(s)); + return _privFilters.yc(_privFilters.yufull(s)); }; @@ -774,7 +318,7 @@ exports.uriInHTMLComment = function (s) { * */ exports.uriPathInSingleQuotedAttr = function (s) { - return uriInAttr(s, privFilters.yavs, privFilters.yu); + return uriInAttr(s, _privFilters.yavs, _privFilters.yu); }; /** @@ -800,7 +344,7 @@ exports.uriPathInSingleQuotedAttr = function (s) { * */ exports.uriPathInDoubleQuotedAttr = function (s) { - return uriInAttr(s, privFilters.yavd, privFilters.yu); + return uriInAttr(s, _privFilters.yavd, _privFilters.yu); }; @@ -827,7 +371,7 @@ exports.uriPathInDoubleQuotedAttr = function (s) { * */ exports.uriPathInUnQuotedAttr = function (s) { - return uriInAttr(s, privFilters.yavu, privFilters.yu); + return uriInAttr(s, _privFilters.yavu, _privFilters.yu); }; /** @@ -853,7 +397,7 @@ exports.uriPathInUnQuotedAttr = function (s) { * http://example.com/?{{{uriQueryInHTMLData uri_query}}} * */ -exports.uriPathInHTMLData = privFilters.yu; +exports.uriPathInHTMLData = _privFilters.yu; /** @@ -878,7 +422,7 @@ exports.uriPathInHTMLData = privFilters.yu; * */ exports.uriPathInHTMLComment = function (s) { - return privFilters.yc(privFilters.yu(s)); + return _privFilters.yc(_privFilters.yu(s)); }; @@ -947,7 +491,7 @@ exports.uriQueryInHTMLComment = exports.uriPathInHTMLComment; * */ exports.uriComponentInSingleQuotedAttr = function (s) { - return privFilters.yavs(privFilters.yuc(s)); + return _privFilters.yavs(_privFilters.yuc(s)); }; /** @@ -973,7 +517,7 @@ exports.uriComponentInSingleQuotedAttr = function (s) { * */ exports.uriComponentInDoubleQuotedAttr = function (s) { - return privFilters.yavd(privFilters.yuc(s)); + return _privFilters.yavd(_privFilters.yuc(s)); }; @@ -1000,7 +544,7 @@ exports.uriComponentInDoubleQuotedAttr = function (s) { * */ exports.uriComponentInUnQuotedAttr = function (s) { - return privFilters.yavu(privFilters.yuc(s)); + return _privFilters.yavu(_privFilters.yuc(s)); }; /** @@ -1026,7 +570,7 @@ exports.uriComponentInUnQuotedAttr = function (s) { * http://example.com/#{{{uriComponentInHTMLData uri_fragment}}} * */ -exports.uriComponentInHTMLData = privFilters.yuc; +exports.uriComponentInHTMLData = _privFilters.yuc; /** @@ -1051,7 +595,7 @@ exports.uriComponentInHTMLData = privFilters.yuc; * */ exports.uriComponentInHTMLComment = function (s) { - return privFilters.yc(privFilters.yuc(s)); + return _privFilters.yc(_privFilters.yuc(s)); }; @@ -1083,7 +627,7 @@ exports.uriComponentInHTMLComment = function (s) { * */ exports.uriFragmentInSingleQuotedAttr = function (s) { - return privFilters.yubl(privFilters.yavs(privFilters.yuc(s))); + return _privFilters.yubl(_privFilters.yavs(_privFilters.yuc(s))); }; // uriFragmentInDoubleQuotedAttr @@ -1114,7 +658,7 @@ exports.uriFragmentInSingleQuotedAttr = function (s) { * */ exports.uriFragmentInDoubleQuotedAttr = function (s) { - return privFilters.yubl(privFilters.yavd(privFilters.yuc(s))); + return _privFilters.yubl(_privFilters.yavd(_privFilters.yuc(s))); }; // uriFragmentInUnQuotedAttr @@ -1144,7 +688,7 @@ exports.uriFragmentInDoubleQuotedAttr = function (s) { * */ exports.uriFragmentInUnQuotedAttr = function (s) { - return privFilters.yubl(privFilters.yavu(privFilters.yuc(s))); + return _privFilters.yubl(_privFilters.yavu(_privFilters.yuc(s))); }; diff --git a/src/lib/xssFilters.priv.js b/src/lib/xssFilters.priv.js new file mode 100644 index 0000000..c78cf56 --- /dev/null +++ b/src/lib/xssFilters.priv.js @@ -0,0 +1,296 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ + +/** + * This file is all about undocumented features + * Do not use it, or please use it with extra care + */ +var x = exports._privFilters || exports; + +var LT = /])/g, + SPECIAL_HTML_CHARS = /[&<>"'`]/g, + SPECIAL_COMMENT_CHARS = /(?:\x00|^-*!?>|--!?>|--?!?$|\]>|\]$)/g; + +// var CSS_VALID_VALUE = +// /^(?: +// (?!-*expression)#?[-\w]+ +// |[+-]?(?:\d+|\d*\.\d+)(?:em|ex|ch|rem|px|mm|cm|in|pt|pc|%|vh|vw|vmin|vmax)? +// |!important +// | //empty +// )$/i; +var CSS_VALID_VALUE = /^(?:(?!-*expression)#?[-\w]+|[+-]?(?:\d+|\d*\.\d+)(?:r?em|ex|ch|cm|mm|in|px|pt|pc|%|vh|vw|vmin|vmax)?|!important|)$/i, + // TODO: prevent double css escaping by not encoding \ again, but this may require CSS decoding + // \x7F and \x01-\x1F less \x09 are for Safari 5.0, added []{}/* for unbalanced quote + CSS_DOUBLE_QUOTED_CHARS = /[\x00-\x1F\x7F\[\]{}\\"]/g, + CSS_SINGLE_QUOTED_CHARS = /[\x00-\x1F\x7F\[\]{}\\']/g, + // (, \u207D and \u208D can be used in background: 'url(...)' in IE, assumed all \ chars are encoded by QUOTED_CHARS, and null is already replaced with \uFFFD + // otherwise, use this CSS_BLACKLIST instead (enhance it with url matching): /(?:\\?\(|[\u207D\u208D]|\\0{0,4}28 ?|\\0{0,2}20[78][Dd] ?)+/g + CSS_BLACKLIST = /url[\(\u207D\u208D]+/g, + // this assumes encodeURI() and encodeURIComponent() has escaped 1-32, 127 for IE8 + CSS_UNQUOTED_URL = /['\(\)]/g; // " \ treated by encodeURI() + +// Given a full URI, need to support "[" ( IPv6address ) "]" in URI as per RFC3986 +// Reference: https://tools.ietf.org/html/rfc3986 +var URL_IPV6 = /\/\/%5[Bb]([A-Fa-f0-9:]+)%5[Dd]/; + + +// Reference: http://shazzer.co.uk/database/All/characters-allowd-in-html-entities +// Reference: http://shazzer.co.uk/vector/Characters-allowed-after-ampersand-in-named-character-references +// Reference: http://shazzer.co.uk/database/All/Characters-before-javascript-uri +// Reference: http://shazzer.co.uk/database/All/Characters-after-javascript-uri +// Reference: https://html.spec.whatwg.org/multipage/syntax.html#consume-a-character-reference +// Reference for named characters: https://html.spec.whatwg.org/multipage/entities.json +var URI_BLACKLIST_PROTOCOLS = {'javascript':1, 'data':1, 'vbscript':1, 'mhtml':1, 'x-schema':1, 'file':1}, + URI_PROTOCOL_WHITESPACES = /(?:^[\x00-\x20]+|[\t\n\r\x00]+)/g, + URI_PROTOCOL_ENCODED = /^([\x00-\x20&#a-zA-Z0-9;+-.]*:?)/; + +var strReplace = function (s, regexp, callback) { + return s === undefined ? 'undefined' + : s === null ? 'null' + : s.toString().replace(regexp, callback); + }; + + +function getProtocol(str, skipHtmlDecoding) { + var m = skipHtmlDecoding || str.match(URI_PROTOCOL_ENCODED), i; + if (m) { + if (!skipHtmlDecoding) { + // getProtocol() must run a greedy html decode algorithm, i.e., omit all NULLs before decoding (as in IE) + // hence, \x00javascript:, &\x00#20;javascript:, and java\x00script: can all return javascript + // and since all NULL is replaced with '', we can set skipNullReplacement in htmlDecode() + str = x.yHtmlDecode(m[1].replace(NULL, ''), true); + } + i = str.indexOf(':'); + if (i !== -1) { + return str.slice(0, i).replace(URI_PROTOCOL_WHITESPACES, '').toLowerCase(); + } + } + return null; +} + + +function cssEncode(chr) { + // space after \\HEX is needed by spec + return '\\' + chr.charCodeAt(0).toString(16).toLowerCase() + ' '; +} +function cssBlacklist(s) { + return s.replace(CSS_BLACKLIST, function(m){ return '-x-' + m; }); +} +function cssUrl(s) { + return s === undefined ? 'undefined' + : s === null ? 'null' + // encodeURI() in yufull() will throw error for use of the CSS_UNSUPPORTED_CODE_POINT (i.e., [\uD800-\uDFFF]) + // prefix ## for blacklisted protocols + // it's safe to skipHtmlDecoding for getProtocol() as s is already html-decoded (and also all NULLs are replaced with \uFFFD) + : URI_BLACKLIST_PROTOCOLS[getProtocol((s = x.yufull(x.yHtmlDecode(s.toString()))), true)] ? '##' + s : s; +} + + + + +/* + * @param {string} s - An untrusted uri input + * @returns {string} s - null if no protocol found, otherwise the protocol with whitespaces stripped and lower-cased + */ +x.yup = getProtocol; + +/* + * @deprecated + * @param {string} s - An untrusted user input + * @returns {string} s - The original user input with & < > " ' ` encoded respectively as & < > " ' and `. + * + */ +x.y = function(s) { + return strReplace(s, SPECIAL_HTML_CHARS, function (m) { + return m === '&' ? '&' + : m === '<' ? '<' + : m === '>' ? '>' + : m === '"' ? '"' + : m === "'" ? ''' + : /*m === '`'*/ '`'; // in hex: 60 + }); +}; + +// This filter is meant to introduce double-encoding, and should be used with extra care. +x.ya = function(s) { + return strReplace(s, AMP, '&'); +}; + +// FOR DETAILS, refer to inHTMLData() +// Reference: https://html.spec.whatwg.org/multipage/syntax.html#data-state +x.yd = function (s) { + return strReplace(s, LT, '<'); +}; + +// FOR DETAILS, refer to inHTMLComment() +// All NULL characters in s are first replaced with \uFFFD. +// If s contains -->, --!>, or starts with -*>, insert a space right before > to stop state breaking at +// If s ends with --!, --, or -, append a space to stop collaborative state breaking at {{{yc s}}}>, {{{yc s}}}!>, {{{yc s}}}-!>, {{{yc s}}}-> +// Reference: https://html.spec.whatwg.org/multipage/syntax.html#comment-state +// Reference: http://shazzer.co.uk/vector/Characters-that-close-a-HTML-comment-3 +// Reference: http://shazzer.co.uk/vector/Characters-that-close-a-HTML-comment +// Reference: http://shazzer.co.uk/vector/Characters-that-close-a-HTML-comment-0021 +// If s contains ]> or ends with ], append a space after ] is verified in IE to stop IE conditional comments. +// Reference: http://msdn.microsoft.com/en-us/library/ms537512%28v=vs.85%29.aspx +// We do not care --\s>, which can possibly be intepreted as a valid close comment tag in very old browsers (e.g., firefox 3.6), as specified in the html4 spec +// Reference: http://www.w3.org/TR/html401/intro/sgmltut.html#h-3.2.4 +x.yc = function (s) { + return strReplace(s, SPECIAL_COMMENT_CHARS, function(m){ + return m === '\x00' ? '\uFFFD' + : m === '--!' || m === '--' || m === '-' || m === ']' ? m + ' ' + :/* + : m === ']>' ? '] >' + : m === '-->' ? '-- >' + : m === '--!>' ? '--! >' + : /-*!?>/.test(m) ? */ m.slice(0, -1) + ' >'; + }); +}; + +// FOR DETAILS, refer to inDoubleQuotedAttr() +// Reference: https://html.spec.whatwg.org/multipage/syntax.html#attribute-value-(double-quoted)-state +x.yavd = function (s) { + return strReplace(s, QUOT, '"'); +}; + +// FOR DETAILS, refer to inSingleQuotedAttr() +// Reference: https://html.spec.whatwg.org/multipage/syntax.html#attribute-value-(single-quoted)-state +x.yavs = function (s) { + return strReplace(s, SQUOT, '''); +}; + +// FOR DETAILS, refer to inUnQuotedAttr() +// PART A. +// if s contains any state breaking chars (\t, \n, \v, \f, \r, space, and >), +// they are escaped and encoded into their equivalent HTML entity representations. +// Reference: http://shazzer.co.uk/database/All/Characters-which-break-attributes-without-quotes +// Reference: https://html.spec.whatwg.org/multipage/syntax.html#attribute-value-(unquoted)-state +// +// PART B. +// if s starts with ', " or `, encode it resp. as ', ", or ` to +// enforce the attr value (unquoted) state +// Reference: https://html.spec.whatwg.org/multipage/syntax.html#before-attribute-value-state +// Reference: http://shazzer.co.uk/vector/Characters-allowed-attribute-quote +// +// PART C. +// Inject a \uFFFD character if an empty or all null string is encountered in +// unquoted attribute value state. +// +// Rationale 1: our belief is that developers wouldn't expect an +// empty string would result in ' name="passwd"' rendered as +// attribute value, even though this is how HTML5 is specified. +// Rationale 2: an empty or all null string (for IE) can +// effectively alter its immediate subsequent state, we choose +// \uFFFD to end the unquoted attr +// state, which therefore will not mess up later contexts. +// Rationale 3: Since IE 6, it is verified that NULL chars are stripped. +// Reference: https://html.spec.whatwg.org/multipage/syntax.html#attribute-value-(unquoted)-state +// +// Example: +// +x.yavu = function (s) { + return strReplace(s, SPECIAL_ATTR_VALUE_UNQUOTED_CHARS, function (m) { + return m === '\t' ? ' ' // in hex: 09 + : m === '\n' ? ' ' // in hex: 0A + : m === '\x0B' ? ' ' // in hex: 0B for IE. IE<9 \v equals v, so use \x0B instead + : m === '\f' ? ' ' // in hex: 0C + : m === '\r' ? ' ' // in hex: 0D + : m === ' ' ? ' ' // in hex: 20 + : m === '=' ? '=' // in hex: 3D + : m === '<' ? '<' + : m === '>' ? '>' + : m === '"' ? '"' + : m === "'" ? ''' + : m === '`' ? '`' + : /*empty or null*/ '\uFFFD'; + }); +}; + +x.yu = encodeURI; +x.yuc = encodeURIComponent; + +// Notice that yubl MUST BE APPLIED LAST, and will not be used independently (expected output from encodeURI/encodeURIComponent and yavd/yavs/yavu) +// This is used to disable JS execution capabilities by prefixing x- to ^javascript:, ^vbscript: or ^data: that possibly could trigger script execution in URI attribute context +x.yubl = function (s) { + // here the output s in either branch will not be html-decoded, + return URI_BLACKLIST_PROTOCOLS[getProtocol(s)] ? 'x-' + s : s; +}; + +// This is NOT a security-critical filter. +// Reference: https://tools.ietf.org/html/rfc3986 +x.yufull = function (s) { + return x.yu(s).replace(URL_IPV6, function(m, p) { + return '//[' + p + ']'; + }); +}; + +// chain yufull() with yubl() +x.yublf = function (s) { + return x.yubl(x.yufull(s)); +}; + +// The design principle of the CSS filter MUST meet the following goal(s). +// (1) The input cannot break out of the context (expr) and this is to fulfill the just sufficient encoding principle. +// (2) The input cannot introduce CSS parsing error and this is to address the concern of UI redressing. +// +// term +// : unary_operator? +// [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* | +// TIME S* | FREQ S* ] +// | STRING S* | IDENT S* | URI S* | hexcolor | function +// +// Reference: +// * http://www.w3.org/TR/CSS21/grammar.html +// * http://www.w3.org/TR/css-syntax-3/ +// +// NOTE: delimiter in CSS - \ _ : ; ( ) " ' / , % # ! * @ . { } +// 2d 5c 5f 3a 3b 28 29 22 27 2f 2c 25 23 21 2a 40 2e 7b 7d + +x.yceu = function(s) { + s = x.yHtmlDecode(s); + return CSS_VALID_VALUE.test(s) ? s : ";-x:'" + cssBlacklist(s.replace(CSS_SINGLE_QUOTED_CHARS, cssEncode)) + "';-v:"; +}; + +// string1 = \"([^\n\r\f\\"]|\\{nl}|\\[^\n\r\f0-9a-f]|\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?)*\" +x.yced = function(s) { + return cssBlacklist(x.yHtmlDecode(s).replace(CSS_DOUBLE_QUOTED_CHARS, cssEncode)); +}; + +// string2 = \'([^\n\r\f\\']|\\{nl}|\\[^\n\r\f0-9a-f]|\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?)*\' +x.yces = function(s) { + return cssBlacklist(x.yHtmlDecode(s).replace(CSS_SINGLE_QUOTED_CHARS, cssEncode)); +}; + +// for url({{{yceuu url}}} +// unquoted_url = ([!#$%&*-~]|\\{h}{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])* (CSS 2.1 definition) +// unquoted_url = ([^"'()\\ \t\n\r\f\v\u0000\u0008\u000b\u000e-\u001f\u007f]|\\{h}{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])* (CSS 3.0 definition) +// The state machine in CSS 3.0 is more well defined - http://www.w3.org/TR/css-syntax-3/#consume-a-url-token0 +// CSS_UNQUOTED_URL = /['\(\)]/g; // " \ treated by encodeURI() +x.yceuu = function(s) { + return cssUrl(s).replace(CSS_UNQUOTED_URL, function (chr) { + return chr === '\'' ? '\\27 ' : + chr === '(' ? '%28' : + /* chr === ')' ? */ '%29'; + }); +}; + +// for url("{{{yceud url}}} +x.yceud = function(s) { + return cssUrl(s); +}; + +// for url('{{{yceus url}}} +x.yceus = function(s) { + return cssUrl(s).replace(SQUOT, '\\27 '); +}; \ No newline at end of file diff --git a/tests/node-unit-tests.js b/tests/node-unit-tests.js index f95e91e..07e1cb6 100644 --- a/tests/node-unit-tests.js +++ b/tests/node-unit-tests.js @@ -12,8 +12,15 @@ Authors: Nera Liu require("mocha"); expect = require('expect.js'); -xssFilters = require('../src/xss-filters'); testutils = require('./utils.js'); -require('./unit/private-xss-filters.js'); -require('./unit/xss-filters.js'); \ No newline at end of file + +xssFilters = require('../'); +require('./unit/xss-filters-private.js'); +require('./unit/xss-filters.js'); + +require('./unit/url-filters-basics.js'); +require('./unit/url-filters-hostParser.js'); +require('./unit/url-filters-parserAPI.js'); +require('./unit/url-filters-withOutput.js'); +require('./unit/url-filters-yUrlResolver.js'); \ No newline at end of file diff --git a/tests/polyfills.js b/tests/polyfills.js index df60701..57d8617 100644 --- a/tests/polyfills.js +++ b/tests/polyfills.js @@ -146,4 +146,90 @@ if (!Array.prototype.map) { // 9. return A return A; }; -} \ No newline at end of file + +} + +// Production steps of ECMA-262, Edition 5, 15.4.4.14 +// Reference: http://es5.github.io/#x15.4.4.14 +if (!Array.prototype.indexOf) { + Array.prototype.indexOf = function(searchElement, fromIndex) { + + var k; + + // 1. Let O be the result of calling ToObject passing + // the this value as the argument. + if (this == null) { + throw new TypeError('"this" is null or not defined'); + } + + var O = Object(this); + + // 2. Let lenValue be the result of calling the Get + // internal method of O with the argument "length". + // 3. Let len be ToUint32(lenValue). + var len = O.length >>> 0; + + // 4. If len is 0, return -1. + if (len === 0) { + return -1; + } + + // 5. If argument fromIndex was passed let n be + // ToInteger(fromIndex); else let n be 0. + var n = +fromIndex || 0; + + if (Math.abs(n) === Infinity) { + n = 0; + } + + // 6. If n >= len, return -1. + if (n >= len) { + return -1; + } + + // 7. If n >= 0, then Let k be n. + // 8. Else, n<0, Let k be len - abs(n). + // If k is less than 0, then let k be 0. + k = Math.max(n >= 0 ? n : len - Math.abs(n), 0); + + // 9. Repeat, while k < len + while (k < len) { + // a. Let Pk be ToString(k). + // This is implicit for LHS operands of the in operator + // b. Let kPresent be the result of calling the + // HasProperty internal method of O with argument Pk. + // This step can be combined with c + // c. If kPresent is true, then + // i. Let elementK be the result of calling the Get + // internal method of O with the argument ToString(k). + // ii. Let same be the result of applying the + // Strict Equality Comparison Algorithm to + // searchElement and elementK. + // iii. If same is true, return k. + if (k in O && O[k] === searchElement) { + return k; + } + k++; + } + return -1; + }; +} + + +/*! JSON v3.3.2 | http://bestiejs.github.io/json3 | Copyright 2012-2014, Kit Cambridge | http://kit.mit-license.org */ +(function(){function N(p,r){function q(a){if(q[a]!==w)return q[a];var c;if("bug-string-char-index"==a)c="a"!="a"[0];else if("json"==a)c=q("json-stringify")&&q("json-parse");else{var e;if("json-stringify"==a){c=r.stringify;var b="function"==typeof c&&s;if(b){(e=function(){return 1}).toJSON=e;try{b="0"===c(0)&&"0"===c(new t)&&'""'==c(new A)&&c(u)===w&&c(w)===w&&c()===w&&"1"===c(e)&&"[1]"==c([e])&&"[null]"==c([w])&&"null"==c(null)&&"[null,null,null]"==c([w,u,null])&&'{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}'== +c({a:[e,!0,!1,null,"\x00\b\n\f\r\t"]})&&"1"===c(null,e)&&"[\n 1,\n 2\n]"==c([1,2],null,1)&&'"-271821-04-20T00:00:00.000Z"'==c(new C(-864E13))&&'"+275760-09-13T00:00:00.000Z"'==c(new C(864E13))&&'"-000001-01-01T00:00:00.000Z"'==c(new C(-621987552E5))&&'"1969-12-31T23:59:59.999Z"'==c(new C(-1))}catch(f){b=!1}}c=b}if("json-parse"==a){c=r.parse;if("function"==typeof c)try{if(0===c("0")&&!c(!1)){e=c('{"a":[1,true,false,null,"\\u0000\\b\\n\\f\\r\\t"]}');var n=5==e.a.length&&1===e.a[0];if(n){try{n=!c('"\t"')}catch(d){}if(n)try{n= +1!==c("01")}catch(g){}if(n)try{n=1!==c("1.")}catch(m){}}}}catch(X){n=!1}c=n}}return q[a]=!!c}p||(p=k.Object());r||(r=k.Object());var t=p.Number||k.Number,A=p.String||k.String,H=p.Object||k.Object,C=p.Date||k.Date,G=p.SyntaxError||k.SyntaxError,K=p.TypeError||k.TypeError,L=p.Math||k.Math,I=p.JSON||k.JSON;"object"==typeof I&&I&&(r.stringify=I.stringify,r.parse=I.parse);var H=H.prototype,u=H.toString,v,B,w,s=new C(-0xc782b5b800cec);try{s=-109252==s.getUTCFullYear()&&0===s.getUTCMonth()&&1===s.getUTCDate()&& +10==s.getUTCHours()&&37==s.getUTCMinutes()&&6==s.getUTCSeconds()&&708==s.getUTCMilliseconds()}catch(Q){}if(!q("json")){var D=q("bug-string-char-index");if(!s)var x=L.floor,M=[0,31,59,90,120,151,181,212,243,273,304,334],E=function(a,c){return M[c]+365*(a-1970)+x((a-1969+(c=+(1d){c+="\\u00"+y(2,d.toString(16));break}c+=f?n[b]:a.charAt(b)}}return c+'"'},O=function(a,c,b,h,f,n,d){var g,m,k,l,p,r,s,t,q;try{g=c[a]}catch(z){}if("object"==typeof g&&g)if(m=u.call(g),"[object Date]"!=m||v.call(g, +"toJSON"))"function"==typeof g.toJSON&&("[object Number]"!=m&&"[object String]"!=m&&"[object Array]"!=m||v.call(g,"toJSON"))&&(g=g.toJSON(a));else if(g>-1/0&&g<1/0){if(E){l=x(g/864E5);for(m=x(l/365.2425)+1970-1;E(m+1,0)<=l;m++);for(k=x((l-E(m,0))/30.42);E(m,k+1)<=l;k++);l=1+l-E(m,k);p=(g%864E5+864E5)%864E5;r=x(p/36E5)%24;s=x(p/6E4)%60;t=x(p/1E3)%60;p%=1E3}else m=g.getUTCFullYear(),k=g.getUTCMonth(),l=g.getUTCDate(),r=g.getUTCHours(),s=g.getUTCMinutes(),t=g.getUTCSeconds(),p=g.getUTCMilliseconds(); +g=(0>=m||1E4<=m?(0>m?"-":"+")+y(6,0>m?-m:m):y(4,m))+"-"+y(2,k+1)+"-"+y(2,l)+"T"+y(2,r)+":"+y(2,s)+":"+y(2,t)+"."+y(3,p)+"Z"}else g=null;b&&(g=b.call(c,a,g));if(null===g)return"null";m=u.call(g);if("[object Boolean]"==m)return""+g;if("[object Number]"==m)return g>-1/0&&g<1/0?""+g:"null";if("[object String]"==m)return R(""+g);if("object"==typeof g){for(a=d.length;a--;)if(d[a]===g)throw K();d.push(g);q=[];c=n;n+=f;if("[object Array]"==m){k=0;for(a=g.length;k=b.length?b:b.slice(0,10));return O("",(l={},l[""]=a,l),f,n,h,"",[])}}if(!q("json-parse")){var V=A.fromCharCode,W={92:"\\",34:'"',47:"/",98:"\b",116:"\t",110:"\n",102:"\f",114:"\r"},b,J,l=function(){b=J=null;throw G();},z=function(){for(var a=J,c=a.length,e,h,f,k,d;bd)l();else if(92==d)switch(d=a.charCodeAt(++b),d){case 92:case 34:case 47:case 98:case 116:case 110:case 102:case 114:e+=W[d];b++;break;case 117:h=++b;for(f=b+4;b=d||97<=d&&102>=d||65<=d&&70>=d||l();e+=V("0x"+a.slice(h,b));break;default:l()}else{if(34==d)break;d=a.charCodeAt(b);for(h=b;32<=d&&92!=d&&34!=d;)d=a.charCodeAt(++b);e+=a.slice(h,b)}if(34==a.charCodeAt(b))return b++,e;l();default:h= +b;45==d&&(k=!0,d=a.charCodeAt(++b));if(48<=d&&57>=d){for(48==d&&(d=a.charCodeAt(b+1),48<=d&&57>=d)&&l();b=d);b++);if(46==a.charCodeAt(b)){for(f=++b;f=d);f++);f==b&&l();b=f}d=a.charCodeAt(b);if(101==d||69==d){d=a.charCodeAt(++b);43!=d&&45!=d||b++;for(f=b;f=d);f++);f==b&&l();b=f}return+a.slice(h,b)}k&&l();if("true"==a.slice(b,b+4))return b+=4,!0;if("false"==a.slice(b,b+5))return b+=5,!1;if("null"==a.slice(b, +b+4))return b+=4,null;l()}return"$"},P=function(a){var c,b;"$"==a&&l();if("string"==typeof a){if("@"==(D?a.charAt(0):a[0]))return a.slice(1);if("["==a){for(c=[];;b||(b=!0)){a=z();if("]"==a)break;b&&(","==a?(a=z(),"]"==a&&l()):l());","==a&&l();c.push(P(a))}return c}if("{"==a){for(c={};;b||(b=!0)){a=z();if("}"==a)break;b&&(","==a?(a=z(),"}"==a&&l()):l());","!=a&&"string"==typeof a&&"@"==(D?a.charAt(0):a[0])&&":"==z()||l();c[a.slice(1)]=P(z())}return c}l()}return a},T=function(a,b,e){e=S(a,b,e);e=== +w?delete a[b]:a[b]=e},S=function(a,b,e){var h=a[b],f;if("object"==typeof h&&h)if("[object Array]"==u.call(h))for(f=h.length;f--;)T(h,f,e);else B(h,function(a){T(h,a,e)});return e.call(a,b,h)};r.parse=function(a,c){var e,h;b=0;J=""+a;e=P(z());"$"!=z()&&l();b=J=null;return c&&"[object Function]"==u.call(c)?S((h={},h[""]=e,h),"",c):e}}}r.runInContext=N;return r}var K=typeof define==="function"&&define.amd,F={"function":!0,object:!0},G=F[typeof exports]&&exports&&!exports.nodeType&&exports,k=F[typeof window]&& +window||this,t=G&&F[typeof module]&&module&&!module.nodeType&&"object"==typeof global&&global;!t||t.global!==t&&t.window!==t&&t.self!==t||(k=t);if(G&&!K)N(k,G);else{var L=k.JSON,Q=k.JSON3,M=!1,A=N(k,k.JSON3={noConflict:function(){M||(M=!0,k.JSON=L,k.JSON3=Q,L=Q=null);return A}});k.JSON={parse:A.parse,stringify:A.stringify}}K&&define(function(){return A})}).call(this); \ No newline at end of file diff --git a/tests/unit/url-filters-basics.js b/tests/unit/url-filters-basics.js new file mode 100644 index 0000000..48b5ceb --- /dev/null +++ b/tests/unit/url-filters-basics.js @@ -0,0 +1,406 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ +(function() { + var _privFilters = xssFilters._privFilters; + var urlFilterFactory = xssFilters.urlFilters.create; + + describe("urlFilterFactory tests", function() { + + it('urlFilterFactory exists', function() { + expect(urlFilterFactory).to.be.ok(); + }); + + it('invalid scheme', function() { + expect(function() { + urlFilterFactory({schemes:['!!']}); + }).to.throwException(); + + // it does not check whether unsafe schemes are used + // expect(function() { + // urlFilterFactory({schemes:['javascript']}); + // }).to.throwException(); + // expect(function() { + // urlFilterFactory({schemes:['vbscript']}); + // }).to.throwException(); + // expect(function() { + // urlFilterFactory({schemes:['data']}); + // }).to.throwException(); + + // expect(function() { + // urlFilterFactory({schemes:['https', 'javascript']}); + // }).to.throwException(); + }); + + it('invalid host', function() { + expect(function() { + urlFilterFactory({hostnames:['yahoo.com:88']}); + }).to.throwException(); + expect(function() { + urlFilterFactory({hostnames:['%9999']}); + }).to.throwException(); + }); + + it('removal of leading whitespaces - positive samples', function() { + [ + '\t\nhttp:www.yahoo.com', + '\rhttp://www.yahoo.com', + '\x00\x03\x20http://www.yahoo.com' + ].forEach(function(url) { + expect(urlFilterFactory()(url)).to.eql(url.replace(/^[\x00-\x20]*/, '')); + }); + }); + + it('removal of leading whitespaces - negative samples', function() { + [ + '\t\nftp:www.yahoo.com', + '\rjavascript:alert(1)', + '\x00\x03\x20mailto:test@yahoo.com' + ].forEach(function(url) { + expect(urlFilterFactory()(url)).to.eql('unsafe:' + url.replace(/^[\x00-\x20]*/, '')); + }); + }); + + + var lastPositiveSamples1 = [ + 'http://www.evil.org/img.jpg', //safe + 'https://yahoo.com', //safe + 'https://hk.finance.yahoo.com', //safe + 'https://finance.yahoo.com.hk', //safe + 'https://finance.yahoo.com.hk/', //safe + 'https://finance.yahoo.com.hk?', //safe + 'https://finance.yahoo.com.hk:', //safe + 'https://finance.yahoo.com.hk#', //safe + 'https://www.yahoo.com', //safe + 'https://www.yahoo.com/', //safe + 'http://www.yahoo.com/', //safe + 'http://www.yahoo.com/subdir/foo.html', //safe + 'http://www.yahoo.com:80', //safe but html encoded link + 'http://example.666.com', //safe but dubious link + 'http://example.com/666.com', //safe but dubious link + 'http://6666.com', //safe but dubious link + 'http://666.com', //safe but dubious link + 'http://666.com/foo', //safe but dubious link + 'http://666.com?foo', //safe but dubious link with args + 'http://666.com:80', //safe but dubious link with ports + 'http://666.com#foo', //safe but dubious link with hash + 'http://666.com\\foo', //safe but dubious link with backslash + 'http://127.0.0.1.com', //safe but dubious link + 'http://127.0.0.1.com/', //safe but dubious link + 'http://127.0.0.1.com?foo', //safe but dubious link with args + 'http://127.0.0.1.com:80', //safe but dubious link with ports + 'http://0x43.0.0x11.com', //safe but dubious link + 'http://0x43.0.0x11.com/', //safe but dubious link + 'http://0x43.0.0x11.com?foo', //safe but dubious link with args + 'http://0x43.0.0x11.com:80', //safe but dubious link with ports + 'http://12798120-foo.com', //safe but dubious decimal link + 'http://0x127981foo.com', //safe but dubious hex link + 'http://foo"onload="alert(0)', //safe but dubious link *1 + 'http:www.yahoo.com', + 'http:\\\\www.yahoo.com', + 'http:/\\/\\www.yahoo.com', + 'http://///www.yahoo.com', + 'http://.www.yahoo.com', + 'http://www.yahoo.com.', + 'http://[2405:3000:3:6::1]', + 'http://notIP.137.189.1.1', + 'http://137.189.1.1' + ]; + var lastNegativeSamples1 = [ + '//www.yahoo.com', // relative protocol + 'javascript:evil();', + 'cid:1234567:111', + 'cid:2:subdir/foo.html', + 'cid:2:/subdir/foo.html', + 'mailto:abc@xyz.org', + 'data:image/jpeg;base64,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=', + 'data:image/png;base64,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=', + 'data:image/gif;base64,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=', + 'h\tttp:www.yahoo.com', + 'h\rt\np:www.yahoo.com', + 'h\rt\np://www.yahoo.com', + 'http://www.yahoo.com', // absolute url but require html decoding + '#a', // relative url, hash + 'img.png', // relative url + '../asdf', + './asdf', + '://www.yahoo.com' + ]; + + + (function() { + var yuwl = urlFilterFactory(); + var p = lastPositiveSamples1.slice(), n = lastNegativeSamples1.slice(); + + it('default - check http and https protocol only - positive samples', function() { + p.forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + + it('default - check http and https protocol only - negative samples', function() { + n.forEach(function(url) { + expect(yuwl(url)).to.eql('unsafe:' + url); + }); + }); + })(); + + (function() { + var yuwl = urlFilterFactory({relScheme: true}); + var p = lastPositiveSamples1.slice(), n = lastNegativeSamples1.slice(); + + it('default + relScheme - allow relative scheme - positive samples', function() { + p.push('//www.yahoo.com'); + + p.forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + it('default + relScheme - allow relative scheme - negative samples', function() { + n.splice(n.indexOf('//www.yahoo.com'), 1); + + n.forEach(function(url) { + expect(yuwl(url)).to.eql('unsafe:' + url); + }); + }); + })(); + + (function() { + var yuwl = urlFilterFactory({relPath: true}); + var p = lastPositiveSamples1.slice(), n = lastNegativeSamples1.slice(); + + // move the last 5 relative path elements from n to p + Array.prototype.push.apply(p, n.slice(n.length - 5)); + n = n.slice(0, n.length - 5); + + // 'http://www.yahoo.com' is considered as a relative path without html decode + // move 'http://www.yahoo.com' from n to p + p.push('http://www.yahoo.com'); + n.splice(n.indexOf('http://www.yahoo.com'), 1); + + it('default + relPath - allow relative path - positive samples', function() { + p.forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + it('default + relPath - allow relative path - negative samples', function() { + n.forEach(function(url) { + expect(yuwl(url)).to.eql('unsafe:' + url); + }); + }); + })(); + + (function() { + var yuwl = urlFilterFactory({relPathOnly: true}); + var p = lastPositiveSamples1.slice(), n = lastNegativeSamples1.slice(); + + // only the last 5 in n can match relPath (i.e., proper inputs) + var positive_ = n.slice(n.length - 5); + n = p.concat(n.slice(0, n.length - 5)); + p = positive_; + + it('default + relPathOnly + htmlDecode - allow relative path ONLY after html decode - positive samples', function() { + p.forEach(function(url) { + expect(yuwl(_privFilters.yHtmlDecode(url))).to.eql(_privFilters.yHtmlDecode(url)); + }); + }); + it('default + relPathOnly + htmlDecode - allow relative path ONLY after html decode - negative samples', function() { + n.forEach(function(url) { + expect(yuwl(_privFilters.yHtmlDecode(url))).to.eql('unsafe:' + _privFilters.yHtmlDecode(url)); + }); + }); + })(); + + (function() { + var yuwl = urlFilterFactory({relPathOnly: true}); + var p = lastPositiveSamples1.slice(), n = lastNegativeSamples1.slice(); + + // only the last 5 in n can match relPath (i.e., proper inputs) + var positive_ = n.slice(n.length - 5); + n = p.concat(n.slice(0, n.length - 5)); + p = positive_; + + // 'http://www.yahoo.com' is considered as a relative path without html decode + // move 'http://www.yahoo.com' from n to p + p.push('http://www.yahoo.com'); + n.splice(n.indexOf('http://www.yahoo.com'), 1); + + it('default + relPathOnly - allow relative path ONLY - positive samples', function() { + p.forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + it('default + relPathOnly - allow relative path ONLY - negative samples', function() { + n.forEach(function(url) { + expect(yuwl(url)).to.eql('unsafe:' + url); + }); + }); + })(); + + (function() { + var yuwl = urlFilterFactory({hostnames: ['666.com']}); + var p = lastPositiveSamples1.slice(), n = lastNegativeSamples1.slice(); + + // filter all samples with 666.com first + var positive_ = []; + p.forEach(function(url){ + if (url.indexOf('666.com') === -1 || + url === 'http://example.666.com' || + url === 'http://example.com/666.com' || + url === 'http://6666.com') { + n.push(url); + } else { + positive_.push(url); + } + }); + p = positive_; + + it('default + options.hostnames - allow only 666.com - positive samples', function() { + p.forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + it('default + options.hostnames - allow only 666.com - negative samples', function() { + n.forEach(function(url) { + expect(yuwl(url)).to.eql('unsafe:' + url); + }); + }); + })(); + + + + + var lastPositiveSamples2 = [ + 'https://yahoo.com', //safe + 'https://hk.finance.yahoo.com', //safe + + 'https://www.yahoo.com', //safe + 'https://www.yahoo.com/', //safe + 'http://www.yahoo.com/', //safe + 'http://www.yahoo.com/subdir/foo.html', //safe + + 'http:www.yahoo.com', + 'http:\\\\www.yahoo.com', + 'http:/\\/\\www.yahoo.com', + 'http://///www.yahoo.com', + 'http://.www.yahoo.com' + ]; + var lastNegativeSamples2 = [ + 'http://www.yahoo.com:80', //safe but html encoded link + '//www.yahoo.com', // relative protocol + 'http://www.yahoo.com.', + 'http://[2405:3000:3:6::1]', + 'http://notIP.137.189.1.1', + 'http://137.189.1.1', + 'javascript:evil();', + 'cid:1234567:111', + 'cid:2:subdir/foo.html', + 'cid:2:/subdir/foo.html', + 'mailto:abc@xyz.org', + 'data:image/jpeg;base64,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=', + 'data:image/png;base64,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=', + 'data:image/gif;base64,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=', + 'h\tttp:www.yahoo.com', + 'h\rt\np:www.yahoo.com', + 'h\rt\np://www.yahoo.com', + 'http://www.yahoo.com', // safe but require html decoding + 'http://www.evil.org/img.jpg', //safe + 'https://finance.yahoo.com.hk', //safe + 'https://finance.yahoo.com.hk/', //safe + 'https://finance.yahoo.com.hk?', //safe + 'https://finance.yahoo.com.hk:', //safe + 'https://finance.yahoo.com.hk#', //safe + 'http://example.666.com', //safe but dubious link + 'http://example.com/666.com', //safe but dubious link + 'http://6666.com', //safe but dubious link + 'http://666.com', //safe but dubious link + 'http://666.com/foo', //safe but dubious link + 'http://666.com?foo', //safe but dubious link with args + 'http://666.com:80', //safe but dubious link with ports + 'http://666.com#foo', //safe but dubious link with hash + 'http://666.com\\foo', //safe but dubious link with backslash + 'http://127.0.0.1.com', //safe but dubious link + 'http://127.0.0.1.com/', //safe but dubious link + 'http://127.0.0.1.com?foo', //safe but dubious link with args + 'http://127.0.0.1.com:80', //safe but dubious link with ports + 'http://0x43.0.0x11.com', //safe but dubious link + 'http://0x43.0.0x11.com/', //safe but dubious link + 'http://0x43.0.0x11.com?foo', //safe but dubious link with args + 'http://0x43.0.0x11.com:80', //safe but dubious link with ports + 'http://12798120-foo.com', //safe but dubious decimal link + 'http://0x127981foo.com', //safe but dubious hex link + 'http://foo"onload="alert(0)', //safe but dubious link *1 + '#a', // relative url, hash + 'img.png', // relative url + '../asdf', + './asdf', + '://www.yahoo.com' + ]; + + + (function() { + var yuwl = urlFilterFactory({hostnames: ['yahoo.com'], subdomain: true}); + var p = lastPositiveSamples2.slice(), n = lastNegativeSamples2.slice(); + + it('default + options.hostnames + options.subdomain - allow (*.)yahoo.com - positive samples', function() { + p.forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + it('default + options.hostnames + options.subdomain - allow (*.)yahoo.com - negative samples', function() { + n.forEach(function(url) { + expect(yuwl(url)).to.eql('unsafe:' + url); + }); + }); + })(); + + (function() { + var yuwl = urlFilterFactory({hostnames: ['yahoo.com'], subdomain: true, relScheme: true}); + var p = lastPositiveSamples2.slice(), n = lastNegativeSamples2.slice(); + + // move //www.yahoo.com from negative to positive + n.splice(n.indexOf('//www.yahoo.com'), 1); + p.push('//www.yahoo.com'); + + it('default + options.hostnames + options.subdomain + options.relScheme - allow (*.)yahoo.com - positive samples', function() { + p.forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + it('default + options.hostnames + options.subdomain + options.relScheme - allow (*.)yahoo.com - negative samples', function() { + n.forEach(function(url) { + expect(yuwl(url)).to.eql('unsafe:' + url); + }); + }); + })(); + + + (function() { + var yuwl = urlFilterFactory({hostnames: ['yahoo.com', '137.189.1.1'], subdomain: true}); + var p = lastPositiveSamples2.slice(), n = lastNegativeSamples2.slice(); + + p.push('http://137.189.1.1'); + n.splice(n.indexOf('http://137.189.1.1'), 1); + + it('default + options.hostnames + options.subdomain - allow (*.)yahoo.com and 137.189.1.1 - positive samples', function() { + p.forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + it('default + options.hostnames + options.subdomain - allow (*.)yahoo.com and 137.189.1.1 - negative samples', function() { + n.forEach(function(url) { + expect(yuwl(url)).to.eql('unsafe:' + url); + }); + }); + + })(); + }); + + +}()); \ No newline at end of file diff --git a/tests/unit/url-filters-hostParser.js b/tests/unit/url-filters-hostParser.js new file mode 100644 index 0000000..c0c9973 --- /dev/null +++ b/tests/unit/url-filters-hostParser.js @@ -0,0 +1,280 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ +(function() { + var _privFilters = xssFilters._privFilters; + var urlFilters = xssFilters.urlFilters; + + describe("hostParser tests", function() { + + + (function() { + it('hostParser exists', function() { + expect(urlFilters.hostParser).to.be.ok(); + }); + + it('invalid ipv6 host', function() { + expect(urlFilters.hostParser('[1:2:3:4:5:6:7:8')).to.eql(null); + }); + })(); + + + var domains = [ + 'http://www.evil.org/img.jpg', //safe + 'https://yahoo.com', //safe + 'https://hk.finance.yahoo.com', //safe + 'https://finance.yahoo.com.hk', //safe + 'https://finance.yahoo.com.hk/', //safe + 'https://finance.yahoo.com.hk?', //safe + 'https://finance.yahoo.com.hk:', //safe + 'https://finance.yahoo.com.hk#', //safe + 'https://www.yahoo.com', //safe + 'https://www.yahoo.com/', //safe + 'http://www.yahoo.com/', //safe + 'http://www.yahoo.com/subdir/foo.html', //safe + 'http://www.yahoo.com:80', //safe but html encoded link + 'http://example.666.com', //safe but dubious link + 'http://example.com/666.com', //safe but dubious link + 'http://6666.com', //safe but dubious link + 'http://666.com', //safe but dubious link + 'http://666.com/foo', //safe but dubious link + 'http://666.com?foo', //safe but dubious link with args + 'http://666.com:80', //safe but dubious link with ports + 'http://666.com#foo', //safe but dubious link with hash + 'http://666.com\\foo', //safe but dubious link with backslash + 'http://127.0.0.1.com', //safe but dubious link + 'http://127.0.0.1.com/', //safe but dubious link + 'http://127.0.0.1.com?foo', //safe but dubious link with args + 'http://127.0.0.1.com:80', //safe but dubious link with ports + 'http://12798120-foo.com', //safe but dubious decimal link + 'http://0x127981foo.com', //safe but dubious hex link + 'http://foo"onload="alert(0)', //safe but dubious link *1 + 'http:www.yahoo.com', + 'http:\\\\www.yahoo.com', + 'http:/\\/\\www.yahoo.com', + 'http://///www.yahoo.com', + 'http://.www.yahoo.com', + 'http://www.yahoo.com.', + ]; + + + var ipv4_Positive = [ + 'http://137.189.1.1.', + 'http://137.189.1.1', + 'http://137.189.1', + 'http://137.2560', + 'http://2560/', + 'http://4294967295/', + 'http://0x1010/', + 'http://9.077.03.0x2/' + ]; + + var ipv4_like_domains = [ + 'http://137.189.1.1.2', + 'http://9.079.03.0x2/', + 'http://9.077.03.0x3H/', + 'http://notIP.137.189.1.1', + 'http://0x43.0.0x11.com', //safe but dubious link + 'http://0x43.0.0x11.com/', //safe but dubious link + 'http://0x43.0.0x11.com?foo', //safe but dubious link with args + 'http://0x43.0.0x11.com:80', //safe but dubious link with ports + ]; + + var ipv4_Negative = [ + 'http://137.189.1.256', + 'http://137.189.256.1', + 'http://137.256.1.1', + 'http://256.1.1.1', + 'http://999.077.03.0x2/', + 'http://4294967296/' + ]; + + + + var ipv6_Positive = [ + 'http://[2405:3000:3:6::1]', + 'http://[1:2:3:4:5:6:7:8]', + 'http://[1:2:3:4:5:6:7::]', + 'http://[::1:2:3:4:5:6:7]', + 'http://[1:2:3:4::6:7:8]', + 'http://[1:2:3:4:5:6:123.123.123.123]', + 'http://[1:2:3:4:5::123.123.123.123]', + 'http://[1:2:3:4::6:123.123.123.123]', + 'http://[1:2:3::5:6:123.123.123.123]', + 'http://[1:2::4:5:6:123.123.123.123]', + 'http://[1::3:4:5:6:123.123.123.123]', + 'http://[::2:3:4:5:6:123.123.123.123]', + 'http://[1:2:3:4::123.123.123.123]', + 'http://[::123.123.123.123]', + 'http://[1::8]', + 'http://[::8]', + 'http://[1::]', + 'http://[::]', + 'http://[1:2:3::6:7:8]', + 'http://[1:2:3:4:5:6:12345.]' + ]; + + var ipv6_Negative = [ + 'http://[1:2:3:4:5:6:7:8/', + 'http://[1:2:3:4:5:6::8/', + 'http://[1:2:3:4:5:6:7:8:9:123.123.123.123]', + 'http://[1:2:3:4:5:6:7:8:123.123.123.123]', + 'http://[1:2:3:4:5:6:7:123.123.123.123]', + 'http://[1:2:3:4:5:6::123.123.123.123]', + 'http://[1:2:3:4:5:6:7:8:9]', + 'http://[1:2:3:4:5:6:7:8::]', + 'http://[1:2:3:4:5:6:7:8:]', + 'http://[::2:3:4:5:6:7:123.123.123.123]', + 'http://[::2:3:4:5:6:7:8:123.123.123.123]', + 'http://[:1:2:3:4:5:6:7:8]', + 'http://[:1:2:3:4:5:6:7:]', + 'http://[1:2:3:4:5:6:7::8]', + 'http://[1:2:3:4:5:6:::8]', + 'http://[:1:2:]', + 'http://[:1:2]', + 'http://[1:2:]', + 'http://[:::]', + 'http://[::::]', + 'http://[::1::]', + 'http://[1:::]', + 'http://[:::2]', + 'http://[123.123.123.123]', + 'http://[::12345]', + 'http://[1:2:3:4:5:6:12345]' + ]; + + + + (function() { + var yuwl = urlFilters.create({ + parseHost: true, + absCallback: function(url, scheme, auth, host, port, path, extra) { + return extra; + }, + unsafeCallback: function(url) { + return null; + } + }); + + it('check domains', function() { + domains.forEach(function(url) { + expect(yuwl(url)).to.have.property('domain'); + }); + ipv4_like_domains.forEach(function(url) { + expect(yuwl(url)).to.have.property('domain'); + }); + }); + + it('check valid ipv4', function() { + ipv4_Positive.forEach(function(url) { + expect(yuwl(url)).to.have.property('ipv4'); + }); + }); + it('check invalid ipv4', function() { + ipv4_Negative.forEach(function(url) { + expect(yuwl(url)).to.eql(null); + }); + ipv4_like_domains.forEach(function(url) { + expect(yuwl(url)).not.to.have.property('ipv4'); + }); + }); + + + it('check valid ipv6', function() { + ipv6_Positive.forEach(function(url) { + expect(yuwl(url)).to.have.property('ipv6'); + }); + }); + it('check invalid ipv6', function() { + ipv6_Negative.forEach(function(url) { + expect(yuwl(url)).to.eql(null); + }); + }); + })(); + + + + (function() { + var yuwl = urlFilters.create({ + hostnames: [ + '[1:2:3:4:5:6:7::]', + '[::1:2:3:4:5:6:7]', + '[::2:3:4:5:6:7]', + '[1:2:3:4:5:6:123.123.123.123]', + '2.153.207.181', + '123456', + 'yahoo.com'], + parseHost: true, + absCallback: function(url, scheme, auth, host, port, path, extra) { + return extra; + }, + unsafeCallback: function(url) { + return null; + } + }); + + it('hostname matching (without subdomain)', function() { + [ + 'http://[1:2:3:4:5:6:7::]', 'http://[1:2:3:4:5:6:7:0]', 'http://[1:2:3:4:5:6:0.7.0]', 'http://[1:2:3:4:5:6:0.7.0x]', + 'http://[::1:2:3:4:5:6:7]', 'http://[0:1:2:3:4:5:6:7]', 'http://%5B0:1:2:3:4%3a5:6:7]', + 'http://[::2:3:4:5:6:7]', + 'http://[1:2:3:4:5:6:123.123.123.123]', 'http://[1:2:3:4:5:6:7b7b:7b7b]', + 'http://2.153.207.181', 'http://43634613', + 'http://123456', 'http://0.1.226.64', 'http://0.1%2e226.64', + 'http://YaHOO.com', 'http://YaHOO%2Ecom' + ].forEach(function(url) { + // console.log(url, yuwl(url)); + expect(yuwl(url)).not.eql(null); + }); + }); + + })(); + + + (function() { + var yuwl = urlFilters.create({ + hostnames: [ + '[1:2:3:4:5:6:7::]', + '[::1:2:3:4:5:6:7]', + '[::2:3:4:5:6:7]', + '[1:2:3:4:5:6:123.123.123.123]', + '2.153.207.181', + '123456', + 'yahoo.com'], + subdomain: true, + parseHost: true, + absCallback: function(url, scheme, auth, host, port, path, extra) { + return extra; + }, + unsafeCallback: function(url) { + return null; + } + }); + + it('hostname matching (without subdomain)', function() { + [ + 'http://[1:2:3:4:5:6:7::]', 'http://[1:2:3:4:5:6:7:0]', 'http://[1:2:3:4:5:6:0.7.0]', 'http://[1:2:3:4:5:6:0.7.0x]', + 'http://[::1:2:3:4:5:6:7]', 'http://[0:1:2:3:4:5:6:7]', 'http://%5B0:1:2:3:4%3a5:6:7]', + 'http://[::2:3:4:5:6:7]', + 'http://[1:2:3:4:5:6:123.123.123.123]', 'http://[1:2:3:4:5:6:7b7b:7b7b]', + 'http://2.153.207.181', 'http://43634613', + 'http://123456', 'http://0.1.226.64', 'http://0.1%2e226.64', + 'http://YaHOO.com', 'http://YaHOO%2Ecom', 'http://www.yahoo.com' + ].forEach(function(url) { + // console.log(url, yuwl(url)); + expect(yuwl(url)).not.eql(null); + }); + }); + + })(); + + }); + + +}()); \ No newline at end of file diff --git a/tests/unit/url-filters-parserAPI.js b/tests/unit/url-filters-parserAPI.js new file mode 100644 index 0000000..094d6b7 --- /dev/null +++ b/tests/unit/url-filters-parserAPI.js @@ -0,0 +1,150 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ +(function() { + var _privFilters = xssFilters._privFilters; + var urlFilterFactory = xssFilters.urlFilters.create; + + describe("urlFilterFactory: URL Parser tests", function() { + var URLParser = urlFilterFactory({ + relScheme: true, + absCallback: function(url, scheme, auth, hostname, port, path){ + return JSON.stringify([url, scheme, auth, hostname, port, path]); + }}); + + it('valid samples', function() { + + // standard + var url = 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'; + var result = [ + url, + 'http:', + 'user:pass', + 'host.com', + '8080', + '/p/a/t/h?query=string#hash']; + + expect(URLParser(url)).to.eql(JSON.stringify(result)); + + // no scheme + result[0] = url = '//user:pass@host.com:8080/p/a/t/h?query=string#hash'; + result[1] = ''; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + + // no port + result[0] = url = 'http://user:pass@host.com/p/a/t/h?query=string#hash'; + result[1] = 'http:'; + result[4] = ''; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + + // no auth and port + result[0] = url = 'http://host.com/p/a/t/h?query=string#hash'; + result[2] = ''; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + + // authority with reserved chars + result[0] = url = 'http://user:p:a@ss@host.com:8080/p/a/t/h?query=string#hash'; + result[2] = 'user:p:a@ss'; + result[4] = '8080'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + + + }); + + it('valid samples with whitespaces within auth, hostname, and port', function() { + var url = 'http://use\r:pass@ho\ts\rt.com:80\n80/p/a/t/h?query=string#hash'; + var result = [ + url, + 'http:', + 'use:pass', + 'host.com', + '8080', + '/p/a/t/h?query=string#hash']; + + expect(URLParser(url)).to.eql(JSON.stringify(result)); + }); + + it('valid samples with various slashes', function() { + var url, result = [ + '', + 'http:', + 'user:pass', + 'host.com', + '8080', + '/p/a/t/h?query=string#hash']; + + // no slashes + result[0] = url = 'http:user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + // more than needed slashes + result[0] = url = 'http:////user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + // back slashes + result[0] = url = 'http:\\\\user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + // more than needed back slashes + result[0] = url = 'http:\\\\\\user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + // mix of slashes + result[0] = url = 'http:\\////\\user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + + // remove the scheme/protocol + result[1] = ''; + // no scheme and more than needed slashes + result[0] = url = '////user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + // no scheme and back slashes + result[0] = url = '\\\\user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + // no scheme and more than needed back slashes + result[0] = url = '\\\\\\user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + // no scheme and mix of slashes + result[0] = url = '\\////\\user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + }); + + it('valid samples with (non-)default port', function() { + var url, result = [ + '', + 'http:', + 'user:pass', + 'host.com', + '8080', + '/p/a/t/h?query=string#hash']; + + // non-default port + result[0] = url = 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + + // default port + result[0] = url = 'http://user:pass@host.com:80/p/a/t/h?query=string#hash'; + result[4] = ''; + expect(URLParser(url)).to.eql(JSON.stringify(result)); + }); + + it('invalid samples', function() { + // url failing the default allowed scheme (http and https) test + var url = 'ht\ttp://user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql('unsafe:' + url); + url = 'ftp://user:pass@host.com:8080/p/a/t/h?query=string#hash'; + expect(URLParser(url)).to.eql('unsafe:' + url); + + + // relative path will get unsafe by default + ['asdf', '/asdf', 'asdf/', '?asdf', '#asdf', '://yahoo.com/hello'].forEach(function(url){ + expect(URLParser(url)).to.eql('unsafe:' + url); + }); + }); + + }); + + +}()); diff --git a/tests/unit/url-filters-withOutput.js b/tests/unit/url-filters-withOutput.js new file mode 100644 index 0000000..d54a0e6 --- /dev/null +++ b/tests/unit/url-filters-withOutput.js @@ -0,0 +1,190 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ +(function() { + + var urlFilterFactory = xssFilters.urlFilters.create; + + var YUWL_WARN_HOST_NUMERIC = 1, + YUWL_WARN_HOST_LOCAL = 2, + YUWL_WARN_PORT_UNCOMMON = 11, + YUWL_WARN_HASH_ONLY = 21; + + // this demonstrates how to further condition urlFilterFactory + var reNumericHost = /^\.?(?:(?:0x[0-9a-f]*|[0-9]+)\.?)*$/i; + + var advConfig = { + schemes: ['http', 'https', 'mailto', 'cid'], + relScheme: true, + imgDataURIs: true, + absCallback: function(url, protocol, authority, host, port, path) { + var httpProtocol = protocol === 'https:' ? 1 : protocol === 'http:' ? 2 : 0; + + if (port) { + return YUWL_WARN_PORT_UNCOMMON; + } + + if (httpProtocol) { + if (reNumericHost.test(host)) { + return YUWL_WARN_HOST_NUMERIC; + } + if (host.indexOf('.') === -1) { + return YUWL_WARN_HOST_LOCAL; + } + } + + return url; + }, + relPath: true, + relCallback: function(url) { + return (url.indexOf('#') === 0) ? YUWL_WARN_HASH_ONLY : url; + } + }; + + describe("urlFilterFactory: output tests", function() { + var yuwl = urlFilterFactory(advConfig); + + it('positive protocol samples', function() { + [ + '//www.yahoo.com', // relative protocol + 'http://www.evil.org/img.jpg', + 'https://www.yahoo.com', + 'https://www.yahoo.com/', + 'http://www.yahoo.com/', + 'http://www.yahoo.com/subdir/foo.html', + 'http://666.com', //safe but dubious link + 'http://666.com/foo', //safe but dubious link + 'http://666.com?foo', //safe but dubious link with args + 'http://666.com:80', //safe but dubious link with ports + 'http://666.com#foo', //safe but dubious link with hash + 'http://666.com\\foo', //safe but dubious link with backslash + 'http://127.0.0.1.com', //safe but dubious link + 'http://127.0.0.1.com/', //safe but dubious link + 'http://127.0.0.1.com?foo', //safe but dubious link with args + 'http://127.0.0.1.com:80', //safe but dubious link with ports + 'http://0x43.0.0x11.com', //safe but dubious link + 'http://0x43.0.0x11.com/', //safe but dubious link + 'http://0x43.0.0x11.com?foo', //safe but dubious link with args + 'http://0x43.0.0x11.com:80', //safe but dubious link with ports + 'http://12798120-foo.com', //safe but dubious decimal link + 'http://0x127981foo.com', //safe but dubious hex link + 'cid:1234567:111', + 'cid:2:subdir/foo.html', + 'cid:2:/subdir/foo.html', + 'mailto:abc@xyz.org', + 'data:image/jpeg;base64,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=', + 'data:image/png;base64,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=', + 'data:image/gif;base64,abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/+=' + ].forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + + it('negative protocol samples', function() { + expect(yuwl('javascript:evil();')).to.eql('unsafe:javascript:evil();'); + }); + + it('positive absCallback samples', function() { + [ + "http://www.yahoo.com/foo", // no port specified + "https://www.yahoo.com/foo", // no port specified + "http://www.yahoo.com:80/foo", // safe port + "https://www.yahoo.com:443/foo", // safe port + 'http://666.com', //safe but dubious link + 'http://666.com/foo', //safe but dubious link + 'http://666.com?foo', //safe but dubious link with args + 'http://666.com:80', //safe but dubious link with ports + 'http://666.com#foo', //safe but dubious link with hash + 'http://666.com\\foo', //safe but dubious link with backslash + 'http://127.0.0.1.com', //safe but dubious link + 'http://127.0.0.1.com/', //safe but dubious link + 'http://127.0.0.1.com?foo', //safe but dubious link with args + 'http://127.0.0.1.com:80', //safe but dubious link with ports + 'http://0x43.0.0x11.com', //safe but dubious link + 'http://0x43.0.0x11.com/', //safe but dubious link + 'http://0x43.0.0x11.com?foo', //safe but dubious link with args + 'http://0x43.0.0x11.com:80', //safe but dubious link with ports + 'http://12798120-foo.com', //safe but dubious decimal link + 'http://0x127981foo.com' //safe but dubious hex link + ].forEach(function(url) { + expect(yuwl(url)).to.eql(url); + }); + }); + + it('negative absCallback samples - port check', function() { + [ + "http://www.yahoo.com:35/foo", // dangerous port + "http://www.yahoo.com:443/foo", // dangerous port + "https://www.yahoo.com:35/foo", // dangerous port + "https://www.yahoo.com:80/foo" // dangerous port + ].forEach(function(url) { + expect(yuwl(url)).to.eql(YUWL_WARN_PORT_UNCOMMON); + }); + }); + + it('negative absCallback samples - numeric hostnames', function() { + [ + "http://127.0.0.1", // dangerous link + "http://127.0.0.1/foo", // dangerous link + "http://127.0.0.1?foo", // dangerous link with args + "http://127.0.0.1:80", // dangerous link with ports + "http://127.0.0.1./foo", // dangerous link with trailing dot + "http://.127.0.0.1/foo", // dangerous link with leading + "http://12798120", // dangerous decimal link + "http://12798120/foo", // dangerous decimal link + "http://12798120?foo", // dangerous decimal link + "http://12798120:80", // dangerous decimal link + "http://0x43.0x19.0x18.0x11", // dangerous hex link + "http://0x43.0x19.0x18.0x11/foo", // dangerous hex link + "http://0x43.0x19.0x18.0x11?foo", // dangerous hex link with args + "http://0x43.0x19.0x18.0x11:80", // dangerous hex link with port + "http://127.0x4319.0x18", // dangerous mixed link + "http://127.0x4319.0x18/foo", // dangerous mixed link + "http://127.0x4319.0x18?foo", // dangerous mixed link with args + "http://127.0x4319.0x18:80" // dangerous mixed link with port + ].forEach(function(url) { + expect(yuwl(url)).to.eql(YUWL_WARN_HOST_NUMERIC); + }); + }); + + it('negative absCallback samples - dubious hostnames', function() { + [ + "http://localhost", + "http://localhost/foo", + "http://localhost?foo", + "http://localhost:80", + "http://somewhere", + "http://somewhere/foo", + "http://somewhere?foo", + "http://somewhere:80", + "http://somewhere#foo", + "http://somewhere\\foo", + 'http://foo"onload="alert(0)' + + ].forEach(function(url) { + expect(yuwl(url)).to.eql(YUWL_WARN_HOST_LOCAL); + }); + }); + + + it('positive relPath samples', function() { + expect(yuwl('img.png')).to.eql('img.png'); + }); + + + it('positive relCallback samples - hash', function() { + expect(yuwl("somewhere#a")).to.eql("somewhere#a"); + }); + + it('negative relCallback samples - hash', function() { + expect(yuwl("#a")).to.eql(YUWL_WARN_HASH_ONLY); + }); + + }); +}()); diff --git a/tests/unit/url-filters-yUrlResolver.js b/tests/unit/url-filters-yUrlResolver.js new file mode 100644 index 0000000..51a75c0 --- /dev/null +++ b/tests/unit/url-filters-yUrlResolver.js @@ -0,0 +1,390 @@ +/* +Copyright (c) 2015, Yahoo! Inc. All rights reserved. +Copyrights licensed under the New BSD License. +See the accompanying LICENSE file for terms. + +Authors: Nera Liu + Adonis Fung + Albert Yu +*/ +(function() { + var _privFilters = xssFilters._privFilters; + var yUrlResolver = xssFilters.urlFilters.yUrlResolver({appendFragment: true}); + + describe("yUrlResolver: Base URL Rewritting", function() { + var relUrls = ['asdf', '/asdf', '?asdf', '#asdf']; + var absUrls = [ + '//www.yahoo.com', + 'http:yahoo.com', + 'https://yahoo.com', + 'https://a@yahoo.com:443']; + var absUrlsAnswers = [ + '//www.yahoo.com/', + 'http://yahoo.com/', + 'https://yahoo.com/', + 'https://a@yahoo.com/']; + var urls = relUrls.concat(absUrls); + + it('empty baseURL samples', function() { + var baseURL = ''; + urls.forEach(function(url){ + expect(yUrlResolver(url, baseURL)).to.eql('unsafe:' + url); + }); + }); + it('invalid scheme baseURL samples', function() { + var baseURL = 'javascript:alert(1)'; + urls.forEach(function(url){ + expect(yUrlResolver(url, baseURL)).to.eql('unsafe:' + url); + }); + }); + it('relative baseURL samples', function() { + var baseURL = '/xyz'; + urls.forEach(function(url){ + expect(yUrlResolver(url, baseURL)).to.eql('unsafe:' + url); + }); + }); + it('schemeless baseURL samples', function() { + var baseURL = '//www.yahoo.com'; + expect(yUrlResolver('asdf', baseURL)).to.eql('//www.yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('//www.yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('//www.yahoo.com/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('//www.yahoo.com/#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + }); + + it('absolute baseURL samples', function() { + var absUrlsAnswers = [ + 'http://www.yahoo.com/', + 'http://yahoo.com/', + 'https://yahoo.com/', + 'https://a@yahoo.com/' + ]; + + var baseURL = 'http://www.yahoo.com'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://www.yahoo.com/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://www.yahoo.com/#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://www.yahoo.com/'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://www.yahoo.com/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://www.yahoo.com/#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://asdf@yahoo.com:80'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://asdf@yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://asdf@yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://asdf@yahoo.com/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://asdf@yahoo.com/#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://@yahoo.com:80'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://yahoo.com/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://yahoo.com/#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://@yahoo.com:81'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://yahoo.com:81/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://yahoo.com:81/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://yahoo.com:81/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://yahoo.com:81/#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://yahoo.com:'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://yahoo.com/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://yahoo.com/#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://www.yahoo.com/finance'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://www.yahoo.com/finance?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://www.yahoo.com/finance#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://www.yahoo.com?finance'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://www.yahoo.com/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://www.yahoo.com/?finance#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://www.yahoo.com#finance'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://www.yahoo.com/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://www.yahoo.com/#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://www.yahoo.com#finance?hello'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://www.yahoo.com/?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://www.yahoo.com/#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + + baseURL = 'http://www.yahoo.com/fin\\ance\\hello?world#test/ing?complex#url'; + expect(yUrlResolver('asdf', baseURL)).to.eql('http://www.yahoo.com/fin/ance/asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('http://www.yahoo.com/fin/ance/hello?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('http://www.yahoo.com/fin/ance/hello?world#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql(absUrlsAnswers[i]); + }); + // last baseURL is being kept + expect(yUrlResolver('asdf')).to.eql('http://www.yahoo.com/fin/ance/asdf'); + expect(yUrlResolver('/asdf')).to.eql('http://www.yahoo.com/asdf'); + expect(yUrlResolver('?asdf')).to.eql('http://www.yahoo.com/fin/ance/hello?asdf'); + expect(yUrlResolver('#asdf')).to.eql('http://www.yahoo.com/fin/ance/hello?world#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url)).to.eql(absUrlsAnswers[i]); + }); + }); + + it('invalid URL samples', function() { + var baseURL = 'http://www.yahoo.com/'; + expect(yUrlResolver('javascript:alert(1)', baseURL)).to.eql('unsafe:javascript:alert(1)'); + }); + + it('invalid baseURL samples', function() { + // invalid baseURL + var baseURL = 'http://yahoo.com:finance'; + expect(yUrlResolver('asdf', baseURL)).to.eql('unsafe:asdf'); + expect(yUrlResolver('/asdf', baseURL)).to.eql('unsafe:/asdf'); + expect(yUrlResolver('?asdf', baseURL)).to.eql('unsafe:?asdf'); + expect(yUrlResolver('#asdf', baseURL)).to.eql('unsafe:#asdf'); + absUrls.forEach(function(url, i){ + expect(yUrlResolver(url, baseURL)).to.eql('unsafe:' + url); + }); + }); + + it('supply baseURL but no url', function() { + var baseURL = 'http://www.yahoo.com/'; + + expect(yUrlResolver('', baseURL)).to.eql(baseURL); + expect(yUrlResolver(undefined, baseURL)).to.eql(baseURL); + + // last baseURL is being kept + expect(yUrlResolver('')).to.eql(baseURL); + expect(yUrlResolver()).to.eql(baseURL); + }); + + it('supply invalid url and no baseURL', function() { + var yUrlResolver = xssFilters.urlFilters.yUrlResolver(); + expect(yUrlResolver('javascript:alert(1)')).to.eql('unsafe:javascript:alert(1)'); + }); + + it('inherit scheme', function() { + expect(yUrlResolver('//hk.yahoo.com?xyz', 'https://yahoo.com?asdf')).to.eql('https://hk.yahoo.com/?xyz'); + expect(yUrlResolver('//hk.yahoo.com?xyz', 'http://yahoo.com?asdf')).to.eql('http://hk.yahoo.com/?xyz'); + }); + + + it('http: and https: absolute URLs and any relative paths (the default)', function() { + var yUrlResolver = xssFilters.urlFilters.yUrlResolver(); + + var baseURL = 'http:yahoo.com'; + expect(yUrlResolver('//hk.yahoo.com?xyz', baseURL)).to.eql('http://hk.yahoo.com/?xyz'); + expect(yUrlResolver('ftp://yahoo.com/?xyz', baseURL)).to.eql('unsafe:ftp://yahoo.com/?xyz'); + expect(yUrlResolver('http://www.yahoo.com/', baseURL)).to.eql('http://www.yahoo.com/'); + expect(yUrlResolver('mailto:abc@xyz.org', baseURL)).to.eql('unsafe:mailto:abc@xyz.org'); + expect(yUrlResolver('cid:1:xyz', baseURL)).to.eql('unsafe:cid:1:xyz'); + expect(yUrlResolver('#abc', baseURL)).to.eql('#abc'); + expect(yUrlResolver('hello/world.html', baseURL)).to.eql('http://yahoo.com/hello/world.html'); + expect(yUrlResolver('/hello/world.html', baseURL)).to.eql('http://yahoo.com/hello/world.html'); + }); + + it('relative path resolution', function() { + var yUrlResolver = xssFilters.urlFilters.yUrlResolver(); + yUrlResolver('', 'http:yahoo.com'); // set baseURL + + expect(yUrlResolver('../../hello/world.html')).to.eql('http://yahoo.com/hello/world.html'); + + expect(yUrlResolver('/hello3/hello2/../../..')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../.%2e')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../%2e.')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../%2e%2e')).to.eql('http://yahoo.com/'); + + expect(yUrlResolver('/hello3/hello2/../../../')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../.%2E/')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../%2E./')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../%2E%2e/')).to.eql('http://yahoo.com/'); + + expect(yUrlResolver('/hello3/hello2/../../../hello')).to.eql('http://yahoo.com/hello'); + expect(yUrlResolver('/hello3/hello2/../../..?hello')).to.eql('http://yahoo.com/?hello'); + expect(yUrlResolver('/hello3/hello2/../../..#hello')).to.eql('http://yahoo.com/#hello'); + + expect(yUrlResolver('/hello3/hello2/../../.')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../%2e')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../%2E')).to.eql('http://yahoo.com/'); + + expect(yUrlResolver('/hello3/hello2/../.././')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../%2e/')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../%2E/')).to.eql('http://yahoo.com/'); + + expect(yUrlResolver('/hello3/hello2/../.././hello')).to.eql('http://yahoo.com/hello'); + expect(yUrlResolver('/hello3/hello2/../../.?hello')).to.eql('http://yahoo.com/?hello'); + expect(yUrlResolver('/hello3/hello2/../../.#hello')).to.eql('http://yahoo.com/#hello'); + + expect(yUrlResolver('/hello2/../hello/world.html')).to.eql('http://yahoo.com/hello/world.html'); + expect(yUrlResolver('/hello2/../hello/./world.html')).to.eql('http://yahoo.com/hello/world.html'); + + expect(yUrlResolver('/hello/#/hello2/world.html')).to.eql('http://yahoo.com/hello/#/hello2/world.html'); + expect(yUrlResolver('/hello/?hello2/world.html')).to.eql('http://yahoo.com/hello/?hello2/world.html'); + expect(yUrlResolver('/hello/#/hello2?world.html')).to.eql('http://yahoo.com/hello/#/hello2?world.html'); + + + yUrlResolver('', 'http:yahoo.com/hello9'); // set baseURL + expect(yUrlResolver('../../hello/world.html')).to.eql('http://yahoo.com/hello/world.html'); + expect(yUrlResolver('hello3/hello2/../..')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('hello3/hello2/../../')).to.eql('http://yahoo.com/'); + + yUrlResolver('', 'http:yahoo.com/hello9/'); // set baseURL + expect(yUrlResolver('../../hello/world.html')).to.eql('http://yahoo.com/hello/world.html'); + expect(yUrlResolver('hello3/hello2/../..')).to.eql('http://yahoo.com/hello9/'); + expect(yUrlResolver('hello3/hello2/../../')).to.eql('http://yahoo.com/hello9/'); + expect(yUrlResolver('hello3/hello2/../hello')).to.eql('http://yahoo.com/hello9/hello3/hello'); + expect(yUrlResolver('hello3/hello2/../..?hello')).to.eql('http://yahoo.com/hello9/?hello'); + expect(yUrlResolver('hello3/hello2/../..#hello')).to.eql('http://yahoo.com/hello9/#hello'); + + expect(yUrlResolver('/hello3/hello2/../..')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../../')).to.eql('http://yahoo.com/'); + expect(yUrlResolver('/hello3/hello2/../hello')).to.eql('http://yahoo.com/hello3/hello'); + expect(yUrlResolver('/hello3/hello2/../..?hello')).to.eql('http://yahoo.com/?hello'); + expect(yUrlResolver('/hello3/hello2/../..#hello')).to.eql('http://yahoo.com/#hello'); + + yUrlResolver('', 'http:yahoo.com/#hello?world/'); // set baseURL + expect(yUrlResolver('?hello')).to.eql('http://yahoo.com/?hello'); + expect(yUrlResolver('#hello')).to.eql('#hello'); + + + yUrlResolver('', 'http://yahoo.com/hello1/hello2/'); // set baseURL + expect(yUrlResolver('foo/bar.html')).to.eql('http://yahoo.com/hello1/hello2/foo/bar.html'); + expect(yUrlResolver('hello/../world.html')).to.eql('http://yahoo.com/hello1/hello2/world.html'); + expect(yUrlResolver('/hello/../world.html')).to.eql('http://yahoo.com/world.html'); + + yUrlResolver('', 'http://yahoo.com/hello1/hello2/..'); // set baseURL + expect(yUrlResolver('foo/bar.html')).to.eql('http://yahoo.com/hello1/foo/bar.html'); + expect(yUrlResolver('hello/../world.html')).to.eql('http://yahoo.com/hello1/world.html'); + expect(yUrlResolver('/hello/../world.html')).to.eql('http://yahoo.com/world.html'); + }); + + + it('relative path resolution (turned off)', function() { + var yUrlResolver = xssFilters.urlFilters.yUrlResolver({resolvePath: false}); + + var baseURL = 'http://yahoo.com/hello1/hello2/'; + expect(yUrlResolver('foo/bar.html', baseURL)).to.eql('http://yahoo.com/hello1/hello2/foo/bar.html'); + expect(yUrlResolver('hello/../world.html', baseURL)).to.eql('http://yahoo.com/hello1/hello2/hello/../world.html'); + expect(yUrlResolver('/hello/../world.html', baseURL)).to.eql('http://yahoo.com/hello/../world.html'); + + var baseURL = 'http://yahoo.com/hello1/hello2/..'; + expect(yUrlResolver('foo/bar.html', baseURL)).to.eql('http://yahoo.com/hello1/hello2/../foo/bar.html'); + expect(yUrlResolver('hello/../world.html', baseURL)).to.eql('http://yahoo.com/hello1/hello2/../hello/../world.html'); + expect(yUrlResolver('/hello/../world.html', baseURL)).to.eql('http://yahoo.com/hello/../world.html'); + }); + + + it('mailto: scheme URLs and any relative paths', function() { + function absBypass(url, origin, scheme, path) { return origin + path; } + function relBypass(path) { return path; } + var yUrlResolver = xssFilters.urlFilters.yUrlResolver({ + relScheme: false, + schemes: ['mailto'], + absResolver: absBypass, + relResolver: relBypass + }); + + var baseURL = 'mailto:qwer@xyz.org'; + expect(yUrlResolver('//hk.yahoo.com?xyz', baseURL)).to.eql('unsafe://hk.yahoo.com?xyz'); + expect(yUrlResolver('ftp://yahoo.com/?xyz', baseURL)).to.eql('unsafe:ftp://yahoo.com/?xyz'); + expect(yUrlResolver('http://www.yahoo.com/', baseURL)).to.eql('unsafe:http://www.yahoo.com/'); + expect(yUrlResolver('mailto:abc@xyz.org', baseURL)).to.eql('mailto:abc@xyz.org'); + expect(yUrlResolver('cid:1:xyz', baseURL)).to.eql('unsafe:cid:1:xyz'); + expect(yUrlResolver('#abc', baseURL)).to.eql('#abc'); + expect(yUrlResolver('hello/world.html', baseURL)).to.eql('hello/world.html'); + expect(yUrlResolver('/hello/world.html', baseURL)).to.eql('/hello/world.html'); + }); + + it('customized schemes', function() { + function absBypass(url, origin, scheme, path) { return origin + path; } + function relBypass(path) { return path; } + var yUrlResolver = xssFilters.urlFilters.yUrlResolver({ + schemes: ['http', 'https', 'ftp', 'mailto', 'cid'], + absResolver: {'mailto:': absBypass, 'cid:': absBypass}, + relResolver: {'mailto:': relBypass, 'cid:': function(path, baseOrigin, baseScheme, basePath, options) { + // 35 is charcode of # + return path.charCodeAt(0) === 35 ? path : baseOrigin + basePath + path; + }} + }); + + var baseURL = '//yahoo.com?asdf'; + expect(yUrlResolver('//hk.yahoo.com?xyz', baseURL)).to.eql('//hk.yahoo.com/?xyz'); + expect(yUrlResolver('ftp://yahoo.com/?xyz', baseURL)).to.eql('ftp://yahoo.com/?xyz'); + expect(yUrlResolver('http://www.yahoo.com/', baseURL)).to.eql('http://www.yahoo.com/'); + expect(yUrlResolver('mailto:abc@xyz.org', baseURL)).to.eql('mailto:abc@xyz.org'); + expect(yUrlResolver('#abc', baseURL)).to.eql('#abc'); + expect(yUrlResolver('hello/world.html', baseURL)).to.eql('//yahoo.com/hello/world.html'); + expect(yUrlResolver('/hello/world.html', baseURL)).to.eql('//yahoo.com/hello/world.html'); + + var baseURL = 'ftp://yahoo.com?asdf'; + expect(yUrlResolver('//hk.yahoo.com?xyz', baseURL)).to.eql('ftp://hk.yahoo.com/?xyz'); + expect(yUrlResolver('ftp://yahoo.com/?xyz', baseURL)).to.eql('ftp://yahoo.com/?xyz'); + expect(yUrlResolver('http://www.yahoo.com/', baseURL)).to.eql('http://www.yahoo.com/'); + expect(yUrlResolver('mailto:abc@xyz.org', baseURL)).to.eql('mailto:abc@xyz.org'); + expect(yUrlResolver('#abc', baseURL)).to.eql('#abc'); + expect(yUrlResolver('hello/world.html', baseURL)).to.eql('ftp://yahoo.com/hello/world.html'); + expect(yUrlResolver('/hello/world.html', baseURL)).to.eql('ftp://yahoo.com/hello/world.html'); + + var baseURL = 'mailto:qwer@xyz.org'; + expect(yUrlResolver('//hk.yahoo.com?xyz', baseURL)).to.eql('//hk.yahoo.com/?xyz'); + expect(yUrlResolver('ftp://yahoo.com/?xyz', baseURL)).to.eql('ftp://yahoo.com/?xyz'); + expect(yUrlResolver('http://www.yahoo.com/', baseURL)).to.eql('http://www.yahoo.com/'); + expect(yUrlResolver('mailto:abc@xyz.org', baseURL)).to.eql('mailto:abc@xyz.org'); + expect(yUrlResolver('cid:1:xyz', baseURL)).to.eql('cid:1:xyz'); + expect(yUrlResolver('#abc', baseURL)).to.eql('#abc'); + expect(yUrlResolver('hello/world.html', baseURL)).to.eql('hello/world.html'); + expect(yUrlResolver('/hello/world.html', baseURL)).to.eql('/hello/world.html'); + + var baseURL = 'cid:2:'; + expect(yUrlResolver('//hk.yahoo.com?xyz', baseURL)).to.eql('//hk.yahoo.com/?xyz'); + expect(yUrlResolver('ftp://yahoo.com/?xyz', baseURL)).to.eql('ftp://yahoo.com/?xyz'); + expect(yUrlResolver('http://www.yahoo.com/', baseURL)).to.eql('http://www.yahoo.com/'); + expect(yUrlResolver('mailto:abc@xyz.org', baseURL)).to.eql('mailto:abc@xyz.org'); + expect(yUrlResolver('cid:1:xyz', baseURL)).to.eql('cid:1:xyz'); + expect(yUrlResolver('#abc', baseURL)).to.eql('#abc'); + expect(yUrlResolver('hello/world.html', baseURL)).to.eql('cid:2:hello/world.html'); + expect(yUrlResolver('/hello/world.html', baseURL)).to.eql('cid:2:/hello/world.html'); + }); + + }); +}()); diff --git a/tests/unit/private-xss-filters.js b/tests/unit/xss-filters-private.js similarity index 99% rename from tests/unit/private-xss-filters.js rename to tests/unit/xss-filters-private.js index d740db3..401edac 100644 --- a/tests/unit/private-xss-filters.js +++ b/tests/unit/xss-filters-private.js @@ -48,7 +48,7 @@ Authors: Nera Liu it('filter yublf exists', function() { expect(filter.yublf).to.be.ok(); }); - + }); describe("private-xss-filters: alias tests", function() { diff --git a/tests/unit/xss-filters.js b/tests/unit/xss-filters.js index c27f953..d3ba73a 100644 --- a/tests/unit/xss-filters.js +++ b/tests/unit/xss-filters.js @@ -11,9 +11,6 @@ Authors: Nera Liu var filter = xssFilters; - delete filter._privFilters; - delete filter._getPrivFilters; - describe("xss-filters: existence tests", function() { it('filter inHTMLData exists', function() { @@ -116,6 +113,18 @@ Authors: Nera Liu expect(filter.uriFragmentInHTMLComment).to.be.ok(); }); + + it('_getPrivFilters functioning', function() { + // _getPrivFilters is visible only for nodejs version, + // and is larged designed for secure-handlebars-helpers + if (filter._getPrivFilters) { + var f = eval(filter._getPrivFilters.toString() + '()'); + ['yd', 'yc', 'yavd', 'yavs', 'yavu', 'yu', 'yuc', 'yubl', 'yufull', 'yceu', 'yced', 'yces', 'yceuu', 'yceud', 'yceus'].forEach(function(exposed){ + expect(f[exposed]).to.be.ok(); + }); + } + }); + }); describe("xss-filters: alias tests", function() { @@ -147,8 +156,10 @@ Authors: Nera Liu describe("xss-filters: error tests", function() { it('filters handling of undefined input', function() { - for (var f in filter) + for (var f in filter) { + f !== '_privFilters' && f !== '_getPrivFilters' && f !== 'urlFilters' && expect(filter[f]()).to.eql('undefined'); + } }); });