diff --git a/karma.conf.js b/karma.conf.js index 1fda181b9f..c3da490397 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -18,6 +18,7 @@ module.exports = function(config) { 'misc/test-lib/jquery-1.8.2.min.js', 'node_modules/angular/angular.js', 'node_modules/angular-mocks/angular-mocks.js', + 'node_modules/angular-sanitize/angular-sanitize.js', 'misc/test-lib/helpers.js', 'src/**/*.js', 'template/**/*.js' diff --git a/misc/demo/assets/app.js b/misc/demo/assets/app.js index af0c4a8ff1..8d19f5481b 100644 --- a/misc/demo/assets/app.js +++ b/misc/demo/assets/app.js @@ -1,5 +1,5 @@ /* global FastClick, smoothScroll */ -angular.module('ui.bootstrap.demo', ['ui.bootstrap', 'plunker', 'ngTouch', 'ngAnimate'], function($httpProvider){ +angular.module('ui.bootstrap.demo', ['ui.bootstrap', 'plunker', 'ngTouch', 'ngAnimate', 'ngSanitize'], function($httpProvider){ FastClick.attach(document.body); delete $httpProvider.defaults.headers.common['X-Requested-With']; }).run(['$location', function($location){ diff --git a/misc/demo/index.html b/misc/demo/index.html index 9b5e683239..6cd3b1db0f 100644 --- a/misc/demo/index.html +++ b/misc/demo/index.html @@ -14,6 +14,7 @@ + diff --git a/package.json b/package.json index bf8aa92bec..fab1235491 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "angular": "^1.4.3", "angular-mocks": "^1.4.3", + "angular-sanitize": "^1.4.3", "grunt": "^0.4.5", "grunt-contrib-concat": "^0.5.1", "grunt-contrib-copy": "^0.8.0", diff --git a/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js new file mode 100644 index 0000000000..17c445528a --- /dev/null +++ b/src/typeahead/test/typeahead-highlight-ngsanitize.spec.js @@ -0,0 +1,17 @@ +describe('Security concerns', function() { + var highlightFilter, $sanitize, logSpy; + + beforeEach(module('ui.bootstrap.typeahead', 'ngSanitize')); + + beforeEach(inject(function (typeaheadHighlightFilter, _$sanitize_, $log) { + highlightFilter = typeaheadHighlightFilter; + $sanitize = _$sanitize_; + logSpy = spyOn($log, 'warn'); + })); + + it('should not call the $log service when ngSanitize is present', function() { + highlightFilter('before after', 'match'); + expect(logSpy).not.toHaveBeenCalled(); + }); + +}); \ No newline at end of file diff --git a/src/typeahead/test/typeahead-highlight.spec.js b/src/typeahead/test/typeahead-highlight.spec.js index 1a5d8b1c5d..1f659bc96b 100644 --- a/src/typeahead/test/typeahead-highlight.spec.js +++ b/src/typeahead/test/typeahead-highlight.spec.js @@ -1,38 +1,52 @@ -describe('typeaheadHighlight', function() { - var highlightFilter; +describe('typeaheadHighlight', function () { + + var highlightFilter, $log, $sce, logSpy; beforeEach(module('ui.bootstrap.typeahead')); - beforeEach(inject(function(typeaheadHighlightFilter) { + + beforeEach(inject(function(_$log_, _$sce_) { + $log = _$log_; + $sce = _$sce_; + logSpy = spyOn($log, 'warn'); + })); + + beforeEach(inject(function (typeaheadHighlightFilter) { highlightFilter = typeaheadHighlightFilter; })); - it('should higlight a match', function() { - expect(highlightFilter('before match after', 'match')).toEqual('before match after'); + it('should higlight a match', function () { + expect($sce.getTrustedHtml(highlightFilter('before match after', 'match'))).toEqual('before match after'); }); - it('should higlight a match with mixed case', function() { - expect(highlightFilter('before MaTch after', 'match')).toEqual('before MaTch after'); + it('should higlight a match with mixed case', function () { + expect($sce.getTrustedHtml(highlightFilter('before MaTch after', 'match'))).toEqual('before MaTch after'); }); - it('should higlight all matches', function() { - expect(highlightFilter('before MaTch after match', 'match')).toEqual('before MaTch after match'); + it('should higlight all matches', function () { + expect($sce.getTrustedHtml(highlightFilter('before MaTch after match', 'match'))).toEqual('before MaTch after match'); }); - it('should do nothing if no match', function() { - expect(highlightFilter('before match after', 'nomatch')).toEqual('before match after'); + it('should do nothing if no match', function () { + expect($sce.getTrustedHtml(highlightFilter('before match after', 'nomatch'))).toEqual('before match after'); }); - it('should do nothing if no or empty query', function() { - expect(highlightFilter('before match after', '')).toEqual('before match after'); - expect(highlightFilter('before match after', null)).toEqual('before match after'); - expect(highlightFilter('before match after', undefined)).toEqual('before match after'); + it('should do nothing if no or empty query', function () { + expect($sce.getTrustedHtml(highlightFilter('before match after', ''))).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', null))).toEqual('before match after'); + expect($sce.getTrustedHtml(highlightFilter('before match after', undefined))).toEqual('before match after'); }); - it('issue 316 - should work correctly for regexp reserved words', function() { - expect(highlightFilter('before (match after', '(match')).toEqual('before (match after'); + it('issue 316 - should work correctly for regexp reserved words', function () { + expect($sce.getTrustedHtml(highlightFilter('before (match after', '(match'))).toEqual('before (match after'); }); - it('issue 1777 - should work correctly with numeric values', function() { - expect(highlightFilter(123, '2')).toEqual('123'); + it('issue 1777 - should work correctly with numeric values', function () { + expect($sce.getTrustedHtml(highlightFilter(123, '2'))).toEqual('123'); }); + + it('should show a warning when this component is being used unsafely', function() { + highlightFilter('before match after', 'match'); + expect(logSpy).toHaveBeenCalled(); + }); + }); diff --git a/src/typeahead/test/typeahead.spec.js b/src/typeahead/test/typeahead.spec.js index 92a75de3f4..015c0a0566 100644 --- a/src/typeahead/test/typeahead.spec.js +++ b/src/typeahead/test/typeahead.spec.js @@ -3,6 +3,7 @@ describe('typeahead tests', function() { var changeInputValueTo; beforeEach(module('ui.bootstrap.typeahead')); + beforeEach(module('ngSanitize')); beforeEach(module('template/typeahead/typeahead-popup.html')); beforeEach(module('template/typeahead/typeahead-match.html')); beforeEach(module(function($compileProvider) { @@ -470,7 +471,7 @@ describe('typeahead tests', function() { expect($scope.isNoResults).toBeFalsy(); })); }); - + describe('select on exact match', function() { it('should select on an exact match when set', function() { $scope.onSelect = jasmine.createSpy('onSelect'); @@ -478,45 +479,45 @@ describe('typeahead tests', function() { var inputEl = findInput(element); changeInputValueTo(element, 'bar'); - + expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); expect(element).toBeClosed(); expect($scope.onSelect).toHaveBeenCalled(); }); - + it('should not select on an exact match by default', function() { $scope.onSelect = jasmine.createSpy('onSelect'); var element = prepareInputEl('
'); var inputEl = findInput(element); - + changeInputValueTo(element, 'bar'); - + expect($scope.result).toBeUndefined(); expect(inputEl.val()).toEqual('bar'); expect($scope.onSelect.calls.any()).toBe(false); }); - + it('should not be case sensitive when select on an exact match', function() { $scope.onSelect = jasmine.createSpy('onSelect'); var element = prepareInputEl('
'); var inputEl = findInput(element); changeInputValueTo(element, 'BaR'); - + expect($scope.result).toEqual('bar'); expect(inputEl.val()).toEqual('bar'); expect(element).toBeClosed(); expect($scope.onSelect).toHaveBeenCalled(); }); - + it('should not auto select when not a match with one potential result left', function() { $scope.onSelect = jasmine.createSpy('onSelect'); var element = prepareInputEl('
'); var inputEl = findInput(element); changeInputValueTo(element, 'fo'); - + expect($scope.result).toBeUndefined(); expect(inputEl.val()).toEqual('fo'); expect($scope.onSelect.calls.any()).toBe(false); diff --git a/src/typeahead/typeahead.js b/src/typeahead/typeahead.js index db661ba0ca..621a498e2f 100644 --- a/src/typeahead/typeahead.js +++ b/src/typeahead/typeahead.js @@ -1,4 +1,4 @@ -angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap.bindHtml']) +angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position']) /** * A helper service that can parse typeahead's syntax (string provided by users) @@ -481,12 +481,29 @@ angular.module('ui.bootstrap.typeahead', ['ui.bootstrap.position', 'ui.bootstrap }; }]) - .filter('typeaheadHighlight', function() { + .filter('typeaheadHighlight', ['$sce', '$injector', '$log', function($sce, $injector, $log) { + + var isSanitizePresent; + isSanitizePresent = $injector.has('$sanitize'); + function escapeRegexp(queryToEscape) { + // Regex: capture the whole query string and replace it with the string that will be used to match + // the results, for example if the capture is "a" the result will be \a return queryToEscape.replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1'); } + function containsHtml(matchItem) { + return /<.*>/g.test(matchItem); + } + return function(matchItem, query) { - return query ? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; + if(!isSanitizePresent && containsHtml(matchItem)) { + $log.warn('Unsafe use of typeahead please use ngSanitize'); // Warn the user about the danger + } + matchItem = query? ('' + matchItem).replace(new RegExp(escapeRegexp(query), 'gi'), '$&') : matchItem; // Replaces the capture string with a the same string inside of a "strong" tag + if(!isSanitizePresent) { + matchItem = $sce.trustAsHtml(matchItem); // If $sanitize is not present we pack the string in a $sce object for the ng-bind-html directive + } + return matchItem; }; - }); + }]); diff --git a/template/typeahead/typeahead-match.html b/template/typeahead/typeahead-match.html index 0711518d9f..f8375988ca 100644 --- a/template/typeahead/typeahead-match.html +++ b/template/typeahead/typeahead-match.html @@ -1 +1 @@ - +