Skip to content

Commit

Permalink
Merge pull request #177 from nealpoole/master
Browse files Browse the repository at this point in the history
Merge in upstream changes from CodeIgniter development branch.
  • Loading branch information
chriso committed Apr 18, 2013
2 parents 305dd30 + c82ca74 commit 478f75c
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 41 deletions.
104 changes: 65 additions & 39 deletions lib/xss.js
Expand Up @@ -4,22 +4,24 @@
var html_entity_decode = require('./entities').decode;

var never_allowed_str = {
'document.cookie': '',
'document.write': '',
'.parentNode': '',
'.innerHTML': '',
'window.location': '',
'-moz-binding': '',
'document.cookie': '[removed]',
'document.write': '[removed]',
'.parentNode': '[removed]',
'.innerHTML': '[removed]',
'window.location': '[removed]',
'-moz-binding': '[removed]',
'<!--': '&lt;!--',
'-->': '--&gt;',
'(<!\\[CDATA\\[)': '&lt;![CDATA['
'(<!\\[CDATA\\[)': '&lt;![CDATA[',
'<comment>': '&lt;comment&gt;'
};

var never_allowed_regex = {
'javascript\\s*:': '',
'expression\\s*(\\(|&\\#40;)': '',
'vbscript\\s*:': '',
'Redirect\\s+302': ''
'javascript\\s*:': '[removed]',
'expression\\s*(\\(|&#40;)': '[removed]',
'vbscript\\s*:': '[removed]',
'Redirect\\s+302': '[removed]',
"([\"'])?data\\s*:[^\\1]*?base64[^\\1]*?,[^\\1]*?\\1?": '[removed]'
};

var non_displayables = [
Expand All @@ -32,8 +34,8 @@ var non_displayables = [

var compact_words = [
'javascript', 'expression', 'vbscript',
'script', 'applet', 'alert', 'document',
'write', 'cookie', 'window'
'script', 'base64', 'applet', 'alert',
'document', 'write', 'cookie', 'window'
];

exports.clean = function(str, is_image) {
Expand All @@ -43,6 +45,8 @@ exports.clean = function(str, is_image) {
for (var i in str) {
str[i] = exports.clean(str[i]);
}
//We emulate the PHP behavior in CodeIgniter.
str.toString = function() { return 'Array'; }
return str;
}

Expand All @@ -55,10 +59,14 @@ exports.clean = function(str, is_image) {
// ensure str does not contain hash before inserting it
hash = xss_hash();
} while(str.indexOf(hash) >= 0)
str = str.replace(/\&([a-z\_0-9]+)\=([a-z\_0-9]+)/ig, hash + '$1=$2');
str = str.replace(/\&([a-z\_0-9\-]+)\=([a-z\_0-9\-]+)/ig, hash + '$1=$2');

//Validate standard character entities. Add a semicolon if missing. We do this to enable
//the conversion of entities to ASCII later.
str = str.replace(/(&#?[0-9a-z]{2,})([\x00-\x20])*;?/ig, '$1;$2');

//Validate UTF16 two byte encoding (x00) - just as above, adds a semicolon if missing.
str = str.replace(/(&\#x?)([0-9A-F]+);?/ig, '$1$2;');
str = str.replace(/(&#x?)([0-9A-F]+);?/ig, '$1$2;');

//Un-protect query string variables
str = str.replace(new RegExp(hash, 'g'), '&');
Expand All @@ -77,6 +85,9 @@ exports.clean = function(str, is_image) {
str = str.replace(/[a-z]+=([\'\"]).*?\1/gi, function(m, match) {
return m.replace(match, convert_attribute(match));
});
str = str.replace(/<\w+.*/gi, function(m) {
return m.replace(m, html_entity_decode(m));
});

//Remove invisible characters again
str = remove_invisible_characters(str);
Expand Down Expand Up @@ -113,41 +124,54 @@ exports.clean = function(str, is_image) {

if (str.match(/<a/i)) {
str = str.replace(/<a\s+([^>]*?)(>|$)/gi, function(m, attributes, end_tag) {
attributes = filter_attributes(attributes.replace('<','').replace('>',''));
if (attributes.match(/href=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)/gi)) {
return m.replace(attributes, '');
}
return m;
var filtered_attributes = filter_attributes(attributes.replace('<','').replace('>',''));
filtered_attributes = filtered_attributes.replace(/href=.*?(?:alert\(|alert&#40;|javascript:|livescript:|mocha:|charset=|window\.|document\.|\.cookie|<script|<xss|data\s*:)/gi, '');
return m.replace(attributes, filtered_attributes);
});
}

if (str.match(/<img/i)) {
str = str.replace(/<img\s+([^>]*?)(\s?\/?>|$)/gi, function(m, attributes, end_tag) {
attributes = filter_attributes(attributes.replace('<','').replace('>',''));
if (attributes.match(/src=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)/gi)) {
return m.replace(attributes, '');
}
return m;
var filtered_attributes = filter_attributes(attributes.replace('<','').replace('>',''));
filtered_attributes = filtered_attributes.replace(/src=.*?(?:alert\(|alert&#40;|javascript:|livescript:|mocha:|charset=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)/gi, '');
return m.replace(attributes, filtered_attributes);
});
}

if (str.match(/script/i) || str.match(/xss/i)) {
str = str.replace(/<(\/*)(script|xss)(.*?)\>/gi, '');
str = str.replace(/<(\/*)(script|xss)(.*?)\>/gi, '[removed]');
}

} while(original != str);
} while(original !== str);

//Remove JavaScript Event Handlers - Note: This code is a little blunt. It removes the event
//handler and anything up to the closing >, but it's unlikely to be a problem.
var event_handlers = ['[^a-z_\-]on\\w*'];
// Remove Evil HTML Attributes (like event handlers and style)
var event_handlers = ['on\\w*', 'style', 'formaction'];

//Adobe Photoshop puts XML metadata into JFIF images, including namespacing,
//so we have to allow this for images
if (!is_image) {
event_handlers.push('xmlns');
}

str = str.replace(new RegExp("<([^><]+?)("+event_handlers.join('|')+")(\\s*=\\s*[^><]*)([><]*)", 'i'), '<$1$4');
do {
var attribs = [];
var count = 0;

attribs = attribs.concat(str.match(new RegExp("("+event_handlers.join('|')+")\\s*=\\s*(\\x22|\\x27)([^\\2]*?)(\\2)", 'ig')));
attribs = attribs.concat(str.match(new RegExp("("+event_handlers.join('|')+")\\s*=\\s*([^\\s>]*)", 'ig')));
attribs = attribs.filter(function(element) { return element !== null; });

if (attribs.length > 0) {
for (var i = 0; i < attribs.length; ++i) {
attribs[i] = attribs[i].replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g'), '\\$&')
}

str = str.replace(new RegExp("(<?)(\/?[^><]+?)([^A-Za-z<>\\-])(.*?)("+attribs.join('|')+")(.*?)([\\s><]?)([><]*)", 'i'), function(m, a, b, c, d, e, f, g, h) {
++count;
return a + b + ' ' + d + f + g + h;
});
}
} while (count > 0);

//Sanitize naughty HTML elements
//If a tag containing any of the words in the list
Expand Down Expand Up @@ -200,13 +224,15 @@ function convert_attribute(str) {
}

function filter_attributes(str) {
var comments = /\/\*.*?\*\//g;
return str.replace(/\s*[a-z-]+\s*=\s*'[^']*'/gi, function (m) {
return m.replace(comments, '');
}).replace(/\s*[a-z-]+\s*=\s*"[^"]*"/gi, function (m) {
return m.replace(comments, '');
}).replace(/\s*[a-z-]+\s*=\s*[^\s]+/gi, function (m) {
return m.replace(comments, '');
});
var result = "";

var match = str.match(/\s*[a-z-]+\s*=\s*(\x22|\x27)([^\1]*?)\1/ig);
if (match) {
for (var i = 0; i < match.length; ++i) {
result += match[i].replace(/\*.*?\*/g, '');
}
}

return result;
}

18 changes: 16 additions & 2 deletions test/filter.test.js
Expand Up @@ -134,10 +134,24 @@ module.exports = {

'test #xss()': function () {
//Need more tests!
assert.equal(' foobar', Filter.sanitize('javascript : foobar').xss());
assert.equal(' foobar', Filter.sanitize('j a vasc ri pt: foobar').xss());
assert.equal('[removed] foobar', Filter.sanitize('javascript : foobar').xss());
assert.equal('[removed] foobar', Filter.sanitize('j a vasc ri pt: foobar').xss());
assert.equal('<a >some text</a>', Filter.sanitize('<a href="javascript:alert(\'xss\')">some text</a>').xss());

assert.equal('<s <> <s >This is a test</s>', Filter.sanitize('<s <onmouseover="alert(1)"> <s onmouseover="alert(1)">This is a test</s>').xss());
assert.equal('<a >">test</a>', Filter.sanitize('<a href="javascriptJ a V a S c R iPt::alert(1)" "<s>">test</a>').xss());
assert.equal('<div ><h1>You have won</h1>Please click the link and enter your login details: <a href="http://example.com/">http://good.com</a></div>', Filter.sanitize('<div style="z-index: 9999999; background-color: green; width: 100%; height: 100%"><h1>You have won</h1>Please click the link and enter your login details: <a href="http://example.com/">http://good.com</a></div>').xss());
assert.equal('<scrRedirec[removed]t 302ipt type="text/javascript">prompt(1);</scrRedirec[removed]t 302ipt>', Filter.sanitize('<scrRedirecRedirect 302t 302ipt type="text/javascript">prompt(1);</scrRedirecRedirect 302t 302ipt>').xss());
assert.equal('<img src="a" ', Filter.sanitize('<img src="a" onerror=\'eval(atob("cHJvbXB0KDEpOw=="))\'').xss());


// Source: http://blog.kotowicz.net/2012/07/codeigniter-210-xssclean-cross-site.html
assert.equal('<img src=">" >', Filter.sanitize('<img/src=">" onerror=alert(1)>').xss());
assert.equal('<button a=">" autofocus ></button>', Filter.sanitize('<button/a=">" autofocus onfocus=alert&#40;1&#40;></button>').xss());
assert.equal('<button a=">" autofocus >', Filter.sanitize('<button a=">" autofocus onfocus=alert&#40;1&#40;>').xss());
assert.equal('<a target="_blank">clickme in firefox</a>', Filter.sanitize('<a target="_blank" href="data:text/html;BASE64youdummy,PHNjcmlwdD5hbGVydCh3aW5kb3cub3BlbmVyLmRvY3VtZW50LmRvY3VtZW50RWxlbWVudC5pbm5lckhUTUwpPC9zY3JpcHQ+">clickme in firefox</a>').xss());
assert.equal('<a/\'\'\' target="_blank" href=[removed]PHNjcmlwdD5hbGVydChvcGVuZXIuZG9jdW1lbnQuYm9keS5pbm5lckhUTUwpPC9zY3JpcHQ+>firefox11</a>', Filter.sanitize('<a/\'\'\' target="_blank" href=data:text/html;;base64,PHNjcmlwdD5hbGVydChvcGVuZXIuZG9jdW1lbnQuYm9keS5pbm5lckhUTUwpPC9zY3JpcHQ+>firefox11</a>').xss());

var url = 'http://www.example.com/test.php?a=b&b=c&c=d';
assert.equal(url, Filter.sanitize(url).xss());
},
Expand Down

0 comments on commit 478f75c

Please sign in to comment.