Permalink
Browse files

Merge in upstream changes from CodeIgniter development branch.

  • Loading branch information...
1 parent 305dd30 commit c82ca7471db66caa7f72dddfeaff639789e7b1ac @nealpoole nealpoole committed Apr 18, 2013
Showing with 81 additions and 41 deletions.
  1. +65 −39 lib/xss.js
  2. +16 −2 test/filter.test.js
View
@@ -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 = [
@@ -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) {
@@ -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;
}
@@ -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'), '&');
@@ -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);
@@ -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
@@ -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;
}
View
@@ -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());
},

0 comments on commit c82ca74

Please sign in to comment.