OS-6: prevent XSS attacks
(i) add sanitize() function to utility function service.
Change-Id: I3f95e46b89f6067f74066e71ae2d736ee0b9ccc2
(cherry picked from commit 69b04c65d7d22d1725b20902cfde12bbf64ebfd4)
diff --git a/web/gui/src/main/webapp/app/fw/util/fn.js b/web/gui/src/main/webapp/app/fw/util/fn.js
index 0d5d4cc..c9cef6d 100644
--- a/web/gui/src/main/webapp/app/fw/util/fn.js
+++ b/web/gui/src/main/webapp/app/fw/util/fn.js
@@ -437,6 +437,64 @@
}
+ // -----------------------------------------------------------------
+ // The next section deals with sanitizing external strings destined
+ // to be loaded via a .html() function call.
+
+ var matcher = /<\/?([a-zA-Z0-9]+)*(.*?)\/?>/igm,
+ whitelist = ['b', 'i', 'p', 'em', 'strong', 'br'],
+ evillist = ['script', 'style', 'iframe'];
+
+ // Returns true if the tag is in the evil list, (and is not an end-tag)
+ function inEvilList(tag) {
+ return (evillist.indexOf(tag.name) !== -1 && tag.full.indexOf('/') === -1);
+ }
+
+ function analyze(html) {
+ html = String(html) || '';
+
+ var matches = [],
+ match;
+
+ // extract all tags
+ while ((match = matcher.exec(html)) !== null) {
+ matches.push({
+ full: match[0],
+ name: match[1]
+ // NOTE: ignoring attributes {match[2].split(' ')} for now
+ });
+ }
+
+ return matches;
+ }
+
+ function sanitize(html) {
+ html = String(html) || '';
+
+ var matches = analyze(html);
+
+ // completely obliterate evil tags and their contents...
+ evillist.forEach(function (tag) {
+ var re = new RegExp('<' + tag + '(.*?)>(.*?[\r\n])*?(.*?)(.*?[\r\n])*?<\/' + tag + '>', 'gim');
+ html = html.replace(re, '');
+ });
+
+ // filter out all but white-listed tags and end-tags
+ matches.forEach(function (tag) {
+ if (whitelist.indexOf(tag.name) === -1) {
+ html = html.replace(tag.full, '');
+ if (inEvilList(tag)) {
+ $log.warn('Unsanitary HTML input -- ' + tag.full + ' detected!');
+ }
+ }
+ });
+
+ // TODO: consider encoding HTML entities, e.g. '&' -> '&'
+
+ return html;
+ }
+
+
angular.module('onosUtil')
.factory('FnService',
['$window', '$location', '$log', function (_$window_, $loc, _$log_) {
@@ -476,7 +534,8 @@
removeFromTrie: removeFromTrie,
trieLookup: trieLookup,
classNames: classNames,
- extend: extend
+ extend: extend,
+ sanitize: sanitize
};
}]);
diff --git a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
index bc434ca..89a4b88 100644
--- a/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
+++ b/web/gui/src/main/webapp/tests/app/fw/util/fn-spec.js
@@ -19,6 +19,7 @@
*/
describe('factory: fw/util/fn.js', function() {
var $window,
+ $log,
fs,
someFunction = function () {},
someArray = [1, 2, 3],
@@ -38,13 +39,22 @@
}
};
+ var mockLog = {
+ debug: function () {},
+ info: function () {},
+ warn: function () {},
+ error: function () {}
+ };
+
beforeEach(function () {
module(function ($provide) {
$provide.value('$window', mockWindow);
+ $provide.value('$log', mockLog);
});
});
- beforeEach(inject(function (_$window_, FnService) {
+ beforeEach(inject(function (_$log_, _$window_, FnService) {
+ $log = _$log_;
$window = _$window_;
fs = FnService;
}));
@@ -217,7 +227,7 @@
'debugOn', 'debug',
'find', 'inArray', 'removeFromArray', 'isEmptyObject', 'sameObjProps', 'containsObj', 'cap',
'eecode', 'noPx', 'noPxStyle', 'endsWith', 'parseBitRate', 'addToTrie', 'removeFromTrie', 'trieLookup',
- 'classNames', 'extend'
+ 'classNames', 'extend', 'sanitize'
])).toBeTruthy();
});
@@ -445,5 +455,68 @@
it('should return 2001', function () {
expect(fs.parseBitRate('2,001.59 Gbps')).toBe(2001);
});
+
+
+ // === Tests for sanitize()
+ function chkSan(u, s) {
+ expect(fs.sanitize(u)).toEqual(s);
+ }
+ function chkGood(g) {
+ chkSan(g, g)
+ }
+ it('should return foo', function () {
+ chkGood('foo');
+ });
+ it('should retain < b > tags', function () {
+ chkGood('foo <b>bar</b> baz');
+ });
+ it('should retain < i > tags', function () {
+ chkGood('foo <i>bar</i> baz');
+ });
+ it('should retain < p > tags', function () {
+ chkGood('foo <p>bar</p> baz');
+ });
+ it('should retain < em > tags', function () {
+ chkGood('foo <em>bar</em> baz');
+ });
+ it('should retain < strong > tags', function () {
+ chkGood('foo <strong>bar</strong> baz');
+ });
+
+ it('should reject < a > tags', function () {
+ chkSan('test <a href="hah">something</a> this', 'test something this');
+ });
+
+ it('should log a warning for < script > tags', function () {
+ spyOn($log, 'warn');
+ chkSan('<script>alert("foo");</script>', '');
+ expect($log.warn).toHaveBeenCalledWith(
+ 'Unsanitary HTML input -- <script> detected!'
+ );
+ });
+ it('should log a warning for < style > tags', function () {
+ spyOn($log, 'warn');
+ chkSan('<style> h1 {color:red;} </style>', '');
+ expect($log.warn).toHaveBeenCalledWith(
+ 'Unsanitary HTML input -- <style> detected!'
+ );
+ });
+
+
+ it('should log a warning for < iframe > tags', function () {
+ spyOn($log, 'warn');
+ chkSan('Foo<iframe><body><h1>fake</h1></body></iframe>Bar', 'FooBar');
+ expect($log.warn).toHaveBeenCalledWith(
+ 'Unsanitary HTML input -- <iframe> detected!'
+ );
+ });
+
+ it('should completely strip < script >, remove < a >, retain < i >', function () {
+ chkSan(
+ 'Hey <i>this</i> is <script>alert("foo");</script> <a href="meh">cool</a>',
+ 'Hey <i>this</i> is cool'
+ );
+ });
+
});