diff --git a/angularFiles.js b/angularFiles.js index 9aa2ef7eda22..ad968341abf9 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -27,6 +27,7 @@ angularFiles = { 'src/ng/parse.js', 'src/ng/q.js', 'src/ng/rootScope.js', + 'src/ng/sanitizeUri.js', 'src/ng/sce.js', 'src/ng/sniffer.js', 'src/ng/timeout.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index eb97b4c5ff46..d2c325c552f6 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -65,6 +65,7 @@ $ParseProvider, $RootScopeProvider, $QProvider, + $$SanitizeUriProvider, $SceProvider, $SceDelegateProvider, $SnifferProvider, @@ -136,6 +137,10 @@ function publishExternalAPI(angular){ angularModule('ng', ['ngLocale'], ['$provide', function ngModule($provide) { + // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it. + $provide.provider({ + $$sanitizeUri: $$SanitizeUriProvider + }); $provide.provider('$compile', $CompileProvider). directive({ a: htmlAnchorDirective, diff --git a/src/ng/compile.js b/src/ng/compile.js index 13fb96827111..54d2dc9f3127 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -493,14 +493,12 @@ var $compileMinErr = minErr('$compile'); * * @description */ -$CompileProvider.$inject = ['$provide']; -function $CompileProvider($provide) { +$CompileProvider.$inject = ['$provide', '$$sanitizeUriProvider']; +function $CompileProvider($provide, $$sanitizeUriProvider) { var hasDirectives = {}, Suffix = 'Directive', COMMENT_DIRECTIVE_REGEXP = /^\s*directive\:\s*([\d\w\-_]+)\s+(.*)$/, - CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/, - aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, - imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//; + CLASS_DIRECTIVE_REGEXP = /(([\d\w\-_]+)(?:\:([^;]+))?;?)/; // Ref: http://developers.whatwg.org/webappapis.html#event-handler-idl-attributes // The assumption is that future DOM event attribute names will begin with @@ -584,10 +582,11 @@ function $CompileProvider($provide) { */ this.aHrefSanitizationWhitelist = function(regexp) { if (isDefined(regexp)) { - aHrefSanitizationWhitelist = regexp; + $$sanitizeUriProvider.aHrefSanitizationWhitelist(regexp); return this; + } else { + return $$sanitizeUriProvider.aHrefSanitizationWhitelist(); } - return aHrefSanitizationWhitelist; }; @@ -614,18 +613,18 @@ function $CompileProvider($provide) { */ this.imgSrcSanitizationWhitelist = function(regexp) { if (isDefined(regexp)) { - imgSrcSanitizationWhitelist = regexp; + $$sanitizeUriProvider.imgSrcSanitizationWhitelist(regexp); return this; + } else { + return $$sanitizeUriProvider.imgSrcSanitizationWhitelist(); } - return imgSrcSanitizationWhitelist; }; - this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', '$rootScope', '$document', '$sce', '$animate', + '$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri', function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller, $rootScope, $document, $sce, $animate) { + $controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) { var Attributes = function(element, attr) { this.$$element = element; @@ -730,16 +729,7 @@ function $CompileProvider($provide) { // sanitize a[href] and img[src] values if ((nodeName === 'A' && key === 'href') || (nodeName === 'IMG' && key === 'src')) { - // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. - if (!msie || msie >= 8 ) { - normalizedVal = urlResolve(value).href; - if (normalizedVal !== '') { - if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || - (key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { - this[key] = value = 'unsafe:' + normalizedVal; - } - } - } + this[key] = value = $$sanitizeUri(value, key === 'src'); } if (writeAttr !== false) { diff --git a/src/ng/sanitizeUri.js b/src/ng/sanitizeUri.js new file mode 100644 index 000000000000..973250946ed0 --- /dev/null +++ b/src/ng/sanitizeUri.js @@ -0,0 +1,74 @@ +'use strict'; + +/** + * @description + * Private service to sanitize uris for links and images. Used by $compile and $sanitize. + */ +function $$SanitizeUriProvider() { + var aHrefSanitizationWhitelist = /^\s*(https?|ftp|mailto|tel|file):/, + imgSrcSanitizationWhitelist = /^\s*(https?|ftp|file):|data:image\//; + + /** + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during a[href] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to a[href] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.aHrefSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + aHrefSanitizationWhitelist = regexp; + return this; + } + return aHrefSanitizationWhitelist; + }; + + + /** + * @description + * Retrieves or overrides the default regular expression that is used for whitelisting of safe + * urls during img[src] sanitization. + * + * The sanitization is a security measure aimed at prevent XSS attacks via html links. + * + * Any url about to be assigned to img[src] via data-binding is first normalized and turned into + * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist` + * regular expression. If a match is found, the original url is written into the dom. Otherwise, + * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM. + * + * @param {RegExp=} regexp New regexp to whitelist urls with. + * @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for + * chaining otherwise. + */ + this.imgSrcSanitizationWhitelist = function(regexp) { + if (isDefined(regexp)) { + imgSrcSanitizationWhitelist = regexp; + return this; + } + return imgSrcSanitizationWhitelist; + }; + + this.$get = function() { + return function sanitizeUri(uri, isImage) { + var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist; + var normalizedVal; + // NOTE: urlResolve() doesn't support IE < 8 so we don't sanitize for that case. + if (!msie || msie >= 8 ) { + normalizedVal = urlResolve(uri).href; + if (normalizedVal !== '' && !normalizedVal.match(regex)) { + return 'unsafe:'+normalizedVal; + } + } + return uri; + }; + }; +} diff --git a/src/ngSanitize/filter/linky.js b/src/ngSanitize/filter/linky.js index 39494f2d3c02..2c05d84e5c1f 100644 --- a/src/ngSanitize/filter/linky.js +++ b/src/ngSanitize/filter/linky.js @@ -1,6 +1,6 @@ 'use strict'; -/* global htmlSanitizeWriter: false */ +/* global sanitizeText: false */ /** * @ngdoc filter @@ -100,7 +100,7 @@ */ -angular.module('ngSanitize').filter('linky', function() { +angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) { var LINKY_URL_REGEXP = /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>]/, MAILTO_REGEXP = /^mailto:/; @@ -110,28 +110,40 @@ angular.module('ngSanitize').filter('linky', function() { var match; var raw = text; var html = []; - // TODO(vojta): use $sanitize instead - var writer = htmlSanitizeWriter(html); var url; var i; - var properties = {}; - if (angular.isDefined(target)) { - properties.target = target; - } while ((match = raw.match(LINKY_URL_REGEXP))) { // We can not end in these as they are sometimes found at the end of the sentence url = match[0]; // if we did not match ftp/http/mailto then assume mailto if (match[2] == match[3]) url = 'mailto:' + url; i = match.index; - writer.chars(raw.substr(0, i)); - properties.href = url; - writer.start('a', properties); - writer.chars(match[0].replace(MAILTO_REGEXP, '')); - writer.end('a'); + addText(raw.substr(0, i)); + addLink(url, match[0].replace(MAILTO_REGEXP, '')); raw = raw.substring(i + match[0].length); } - writer.chars(raw); - return html.join(''); + addText(raw); + return $sanitize(html.join('')); + + function addText(text) { + if (!text) { + return; + } + html.push(sanitizeText(text)); + } + + function addLink(url, text) { + html.push(''); + addText(text); + html.push(''); + } }; -}); +}]); diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js index 7bd9aae3c761..5d378b02cac3 100644 --- a/src/ngSanitize/sanitize.js +++ b/src/ngSanitize/sanitize.js @@ -46,6 +46,8 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); * it into the returned string, however, since our parser is more strict than a typical browser * parser, it's possible that some obscure input, which would be recognized as valid HTML by a * browser, won't make it through the sanitizer. + * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and + * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. * * @param {string} html Html input. * @returns {string} Sanitized html. @@ -128,11 +130,24 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize'); */ -var $sanitize = function(html) { +function $SanitizeProvider() { + this.$get = ['$$sanitizeUri', function($$sanitizeUri) { + return function(html) { + var buf = []; + htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) { + return !/^unsafe/.test($$sanitizeUri(uri, isImage)); + })); + return buf.join(''); + }; + }]; +} + +function sanitizeText(chars) { var buf = []; - htmlParser(html, htmlSanitizeWriter(buf)); - return buf.join(''); -}; + var writer = htmlSanitizeWriter(buf, angular.noop); + writer.chars(chars); + return buf.join(''); +} // Regular Expressions for parsing tags and attributes @@ -145,7 +160,6 @@ var START_TAG_REGEXP = COMMENT_REGEXP = //g, DOCTYPE_REGEXP = /]*?)>/i, CDATA_REGEXP = //g, - URI_REGEXP = /^((ftp|https?):\/\/|mailto:|tel:|#)/i, // Match everything outside of normal chars and " (quote character) NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g; @@ -353,8 +367,18 @@ function htmlParser( html, handler ) { */ var hiddenPre=document.createElement("pre"); function decodeEntities(value) { - hiddenPre.innerHTML=value.replace(/')($rootScope); $rootScope.testUrl = 'http://example.com/image.png'; @@ -3845,127 +3846,6 @@ describe('$compile', function() { expect(element.attr('src')).toEqual('http://example.com/image2.png'); })); - it('should sanitize javascript: urls', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.testUrl = "javascript:doEvilStuff()"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('unsafe:javascript:doEvilStuff()'); - })); - - it('should sanitize non-image data: urls', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.testUrl = "data:application/javascript;charset=US-ASCII,alert('evil!');"; - $rootScope.$apply(); - expect(element.attr('src')).toBe("unsafe:data:application/javascript;charset=US-ASCII,alert('evil!');"); - $rootScope.testUrl = "data:,foo"; - $rootScope.$apply(); - expect(element.attr('src')).toBe("unsafe:data:,foo"); - })); - - - it('should not sanitize data: URIs for images', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - - // image data uri - // ref: http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever - $rootScope.dataUri = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; - $rootScope.$apply(); - expect(element.attr('src')).toBe('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); - })); - - - // Fails on IE <= 10 with "TypeError: Access is denied" when trying to set img[src] - if (!msie || msie > 10) { - it('should sanitize mailto: urls', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.testUrl = "mailto:foo@bar.com"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('unsafe:mailto:foo@bar.com'); - })); - } - - it('should sanitize obfuscated javascript: urls', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - - // case-sensitive - $rootScope.testUrl = "JaVaScRiPt:doEvilStuff()"; - $rootScope.$apply(); - expect(element[0].src).toBe('unsafe:javascript:doEvilStuff()'); - - // tab in protocol - $rootScope.testUrl = "java\u0009script:doEvilStuff()"; - $rootScope.$apply(); - expect(element[0].src).toMatch(/(http:\/\/|unsafe:javascript:doEvilStuff\(\))/); - - // space before - $rootScope.testUrl = " javascript:doEvilStuff()"; - $rootScope.$apply(); - expect(element[0].src).toBe('unsafe:javascript:doEvilStuff()'); - - // ws chars before - $rootScope.testUrl = " \u000e javascript:doEvilStuff()"; - $rootScope.$apply(); - expect(element[0].src).toMatch(/(http:\/\/|unsafe:javascript:doEvilStuff\(\))/); - - // post-fixed with proper url - $rootScope.testUrl = "javascript:doEvilStuff(); http://make.me/look/good"; - $rootScope.$apply(); - expect(element[0].src).toBeOneOf( - 'unsafe:javascript:doEvilStuff(); http://make.me/look/good', - 'unsafe:javascript:doEvilStuff();%20http://make.me/look/good' - ); - })); - - it('should sanitize ng-src bindings as well', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.testUrl = "javascript:doEvilStuff()"; - $rootScope.$apply(); - - expect(element[0].src).toBe('unsafe:javascript:doEvilStuff()'); - })); - - - it('should not sanitize valid urls', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - - $rootScope.testUrl = "foo/bar"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('foo/bar'); - - $rootScope.testUrl = "/foo/bar"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('/foo/bar'); - - $rootScope.testUrl = "../foo/bar"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('../foo/bar'); - - $rootScope.testUrl = "#foo"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('#foo'); - - $rootScope.testUrl = "http://foo.com/bar"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('http://foo.com/bar'); - - $rootScope.testUrl = " http://foo.com/bar"; - $rootScope.$apply(); - expect(element.attr('src')).toBe(' http://foo.com/bar'); - - $rootScope.testUrl = "https://foo.com/bar"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('https://foo.com/bar'); - - $rootScope.testUrl = "ftp://foo.com/bar"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('ftp://foo.com/bar'); - - $rootScope.testUrl = "file:///foo/bar.html"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('file:///foo/bar.html'); - })); - - it('should not sanitize attributes other than src', inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = "javascript:doEvilStuff()"; @@ -3974,141 +3854,42 @@ describe('$compile', function() { expect(element.attr('title')).toBe('javascript:doEvilStuff()'); })); + it('should use $$sanitizeUriProvider for reconfiguration of the src whitelist', function() { + module(function($compileProvider, $$sanitizeUriProvider) { + var newRe = /javascript:/, + returnVal; + expect($compileProvider.imgSrcSanitizationWhitelist()).toBe($$sanitizeUriProvider.imgSrcSanitizationWhitelist()); - it('should allow reconfiguration of the src whitelist', function() { - module(function($compileProvider) { - expect($compileProvider.imgSrcSanitizationWhitelist() instanceof RegExp).toBe(true); - var returnVal = $compileProvider.imgSrcSanitizationWhitelist(/javascript:/); + returnVal = $compileProvider.imgSrcSanitizationWhitelist(newRe); expect(returnVal).toBe($compileProvider); + expect($$sanitizeUriProvider.imgSrcSanitizationWhitelist()).toBe(newRe); + expect($compileProvider.imgSrcSanitizationWhitelist()).toBe(newRe); }); + inject(function() { + // needed to the module definition above is run... + }); + }); + it('should use $$sanitizeUri', function() { + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); + module(function($provide) { + $provide.value('$$sanitizeUri', $$sanitizeUri); + }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); + $rootScope.testUrl = "someUrl"; - // Fails on IE <= 11 with "TypeError: Object doesn't support this property or method" when - // trying to set img[src] - if (!msie || msie > 11) { - $rootScope.testUrl = "javascript:doEvilStuff()"; - $rootScope.$apply(); - expect(element.attr('src')).toBe('javascript:doEvilStuff()'); - } - - $rootScope.testUrl = "http://recon/figured"; + $$sanitizeUri.andReturn('someSanitizedUrl'); $rootScope.$apply(); - expect(element.attr('src')).toBe('unsafe:http://recon/figured'); + expect(element.attr('src')).toBe('someSanitizedUrl'); + expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, true); }); }); - }); describe('a[href] sanitization', function() { - it('should sanitize javascript: urls', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.testUrl = "javascript:doEvilStuff()"; - $rootScope.$apply(); - - expect(element.attr('href')).toBe('unsafe:javascript:doEvilStuff()'); - })); - - - it('should sanitize data: urls', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.testUrl = "data:evilPayload"; - $rootScope.$apply(); - - expect(element.attr('href')).toBe('unsafe:data:evilPayload'); - })); - - - it('should sanitize obfuscated javascript: urls', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - - // case-sensitive - $rootScope.testUrl = "JaVaScRiPt:doEvilStuff()"; - $rootScope.$apply(); - expect(element[0].href).toBe('unsafe:javascript:doEvilStuff()'); - - // tab in protocol - $rootScope.testUrl = "java\u0009script:doEvilStuff()"; - $rootScope.$apply(); - expect(element[0].href).toMatch(/(http:\/\/|unsafe:javascript:doEvilStuff\(\))/); - - // space before - $rootScope.testUrl = " javascript:doEvilStuff()"; - $rootScope.$apply(); - expect(element[0].href).toBe('unsafe:javascript:doEvilStuff()'); - - // ws chars before - $rootScope.testUrl = " \u000e javascript:doEvilStuff()"; - $rootScope.$apply(); - expect(element[0].href).toMatch(/(http:\/\/|unsafe:javascript:doEvilStuff\(\))/); - - // post-fixed with proper url - $rootScope.testUrl = "javascript:doEvilStuff(); http://make.me/look/good"; - $rootScope.$apply(); - expect(element[0].href).toBeOneOf( - 'unsafe:javascript:doEvilStuff(); http://make.me/look/good', - 'unsafe:javascript:doEvilStuff();%20http://make.me/look/good' - ); - })); - - - it('should sanitize ngHref bindings as well', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - $rootScope.testUrl = "javascript:doEvilStuff()"; - $rootScope.$apply(); - - expect(element[0].href).toBe('unsafe:javascript:doEvilStuff()'); - })); - - - it('should not sanitize valid urls', inject(function($compile, $rootScope) { - element = $compile('')($rootScope); - - $rootScope.testUrl = "foo/bar"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('foo/bar'); - - $rootScope.testUrl = "/foo/bar"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('/foo/bar'); - - $rootScope.testUrl = "../foo/bar"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('../foo/bar'); - - $rootScope.testUrl = "#foo"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('#foo'); - - $rootScope.testUrl = "http://foo/bar"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('http://foo/bar'); - - $rootScope.testUrl = " http://foo/bar"; - $rootScope.$apply(); - expect(element.attr('href')).toBe(' http://foo/bar'); - - $rootScope.testUrl = "https://foo/bar"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('https://foo/bar'); - - $rootScope.testUrl = "ftp://foo/bar"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('ftp://foo/bar'); - - $rootScope.testUrl = "mailto:foo@bar.com"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('mailto:foo@bar.com'); - - $rootScope.testUrl = "file:///foo/bar.html"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('file:///foo/bar.html'); - })); - - it('should not sanitize href on elements other than anchor', inject(function($compile, $rootScope) { element = $compile('
')($rootScope); $rootScope.testUrl = "javascript:doEvilStuff()"; @@ -4117,7 +3898,6 @@ describe('$compile', function() { expect(element.attr('href')).toBe('javascript:doEvilStuff()'); })); - it('should not sanitize attributes other than href', inject(function($compile, $rootScope) { element = $compile('')($rootScope); $rootScope.testUrl = "javascript:doEvilStuff()"; @@ -4126,26 +3906,38 @@ describe('$compile', function() { expect(element.attr('title')).toBe('javascript:doEvilStuff()'); })); + it('should use $$sanitizeUriProvider for reconfiguration of the href whitelist', function() { + module(function($compileProvider, $$sanitizeUriProvider) { + var newRe = /javascript:/, + returnVal; + expect($compileProvider.aHrefSanitizationWhitelist()).toBe($$sanitizeUriProvider.aHrefSanitizationWhitelist()); - it('should allow reconfiguration of the href whitelist', function() { - module(function($compileProvider) { - expect($compileProvider.aHrefSanitizationWhitelist() instanceof RegExp).toBe(true); - var returnVal = $compileProvider.aHrefSanitizationWhitelist(/javascript:/); + returnVal = $compileProvider.aHrefSanitizationWhitelist(newRe); expect(returnVal).toBe($compileProvider); + expect($$sanitizeUriProvider.aHrefSanitizationWhitelist()).toBe(newRe); + expect($compileProvider.aHrefSanitizationWhitelist()).toBe(newRe); + }); + inject(function() { + // needed to the module definition above is run... }); + }); + it('should use $$sanitizeUri', function() { + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); + module(function($provide) { + $provide.value('$$sanitizeUri', $$sanitizeUri); + }); inject(function($compile, $rootScope) { element = $compile('')($rootScope); + $rootScope.testUrl = "someUrl"; - $rootScope.testUrl = "javascript:doEvilStuff()"; - $rootScope.$apply(); - expect(element.attr('href')).toBe('javascript:doEvilStuff()'); - - $rootScope.testUrl = "http://recon/figured"; + $$sanitizeUri.andReturn('someSanitizedUrl'); $rootScope.$apply(); - expect(element.attr('href')).toBe('unsafe:http://recon/figured'); + expect(element.attr('href')).toBe('someSanitizedUrl'); + expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false); }); }); + }); describe('interpolation on HTML DOM event handler attributes onclick, onXYZ, formaction', function() { diff --git a/test/ng/sanitizeUriSpec.js b/test/ng/sanitizeUriSpec.js new file mode 100644 index 000000000000..b9f6a0e21f19 --- /dev/null +++ b/test/ng/sanitizeUriSpec.js @@ -0,0 +1,230 @@ +'use strict'; + +describe('sanitizeUri', function() { + var sanitizeHref, sanitizeImg, sanitizeUriProvider, testUrl; + beforeEach(function() { + module(function(_$$sanitizeUriProvider_) { + sanitizeUriProvider = _$$sanitizeUriProvider_; + }); + inject(function($$sanitizeUri) { + sanitizeHref = function(uri) { + return $$sanitizeUri(uri, false); + }; + sanitizeImg = function(uri) { + return $$sanitizeUri(uri, true); + } + }); + }); + + function isEvilInCurrentBrowser(uri) { + var a = document.createElement('a'); + a.setAttribute('href', uri); + return a.href.substring(0, 4) !== 'http'; + } + + describe('img[src] sanitization', function() { + + it('should sanitize javascript: urls', function() { + testUrl = "javascript:doEvilStuff()"; + expect(sanitizeImg(testUrl)).toBe('unsafe:javascript:doEvilStuff()'); + }); + + it('should sanitize non-image data: urls', function() { + testUrl = "data:application/javascript;charset=US-ASCII,alert('evil!');"; + expect(sanitizeImg(testUrl)).toBe("unsafe:data:application/javascript;charset=US-ASCII,alert('evil!');"); + + testUrl = "data:,foo"; + expect(sanitizeImg(testUrl)).toBe("unsafe:data:,foo"); + }); + + it('should not sanitize data: URIs for images', function() { + // image data uri + // ref: http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever + testUrl = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + expect(sanitizeImg(testUrl)).toBe('data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); + }); + + it('should sanitize mailto: urls', function() { + testUrl = "mailto:foo@bar.com"; + expect(sanitizeImg(testUrl)).toBe('unsafe:mailto:foo@bar.com'); + }); + + it('should sanitize obfuscated javascript: urls', function() { + // case-sensitive + testUrl = "JaVaScRiPt:doEvilStuff()"; + expect(sanitizeImg(testUrl)).toBe('unsafe:javascript:doEvilStuff()'); + + // tab in protocol + testUrl = "java\u0009script:doEvilStuff()"; + if (isEvilInCurrentBrowser(testUrl)) { + expect(sanitizeImg(testUrl)).toEqual('unsafe:javascript:doEvilStuff()'); + } + + // space before + testUrl = " javascript:doEvilStuff()"; + expect(sanitizeImg(testUrl)).toBe('unsafe:javascript:doEvilStuff()'); + + // ws chars before + testUrl = " \u000e javascript:doEvilStuff()"; + if (isEvilInCurrentBrowser(testUrl)) { + expect(sanitizeImg(testUrl)).toEqual('unsafe:javascript:doEvilStuff()'); + } + + // post-fixed with proper url + testUrl = "javascript:doEvilStuff(); http://make.me/look/good"; + expect(sanitizeImg(testUrl)).toBeOneOf( + 'unsafe:javascript:doEvilStuff(); http://make.me/look/good', + 'unsafe:javascript:doEvilStuff();%20http://make.me/look/good' + ); + }); + + it('should sanitize ng-src bindings as well', function() { + testUrl = "javascript:doEvilStuff()"; + expect(sanitizeImg(testUrl)).toBe('unsafe:javascript:doEvilStuff()'); + }); + + + it('should not sanitize valid urls', function() { + testUrl = "foo/bar"; + expect(sanitizeImg(testUrl)).toBe('foo/bar'); + + testUrl = "/foo/bar"; + expect(sanitizeImg(testUrl)).toBe('/foo/bar'); + + testUrl = "../foo/bar"; + expect(sanitizeImg(testUrl)).toBe('../foo/bar'); + + testUrl = "#foo"; + expect(sanitizeImg(testUrl)).toBe('#foo'); + + testUrl = "http://foo.com/bar"; + expect(sanitizeImg(testUrl)).toBe('http://foo.com/bar'); + + testUrl = " http://foo.com/bar"; + expect(sanitizeImg(testUrl)).toBe(' http://foo.com/bar'); + + testUrl = "https://foo.com/bar"; + expect(sanitizeImg(testUrl)).toBe('https://foo.com/bar'); + + testUrl = "ftp://foo.com/bar"; + expect(sanitizeImg(testUrl)).toBe('ftp://foo.com/bar'); + + testUrl = "file:///foo/bar.html"; + expect(sanitizeImg(testUrl)).toBe('file:///foo/bar.html'); + }); + + + it('should allow reconfiguration of the src whitelist', function() { + var returnVal; + expect(sanitizeUriProvider.imgSrcSanitizationWhitelist() instanceof RegExp).toBe(true); + returnVal = sanitizeUriProvider.imgSrcSanitizationWhitelist(/javascript:/); + expect(returnVal).toBe(sanitizeUriProvider); + + testUrl = "javascript:doEvilStuff()"; + expect(sanitizeImg(testUrl)).toBe('javascript:doEvilStuff()'); + + testUrl = "http://recon/figured"; + expect(sanitizeImg(testUrl)).toBe('unsafe:http://recon/figured'); + }); + + }); + + + describe('a[href] sanitization', function() { + + it('should sanitize javascript: urls', inject(function() { + testUrl = "javascript:doEvilStuff()"; + expect(sanitizeHref(testUrl)).toBe('unsafe:javascript:doEvilStuff()'); + })); + + + it('should sanitize data: urls', inject(function() { + testUrl = "data:evilPayload"; + expect(sanitizeHref(testUrl)).toBe('unsafe:data:evilPayload'); + })); + + + it('should sanitize obfuscated javascript: urls', inject(function() { + // case-sensitive + testUrl = "JaVaScRiPt:doEvilStuff()"; + expect(sanitizeHref(testUrl)).toBe('unsafe:javascript:doEvilStuff()'); + + // tab in protocol + testUrl = "java\u0009script:doEvilStuff()"; + if (isEvilInCurrentBrowser(testUrl)) { + expect(sanitizeHref(testUrl)).toEqual('unsafe:javascript:doEvilStuff()'); + } + + // space before + testUrl = " javascript:doEvilStuff()"; + expect(sanitizeHref(testUrl)).toBe('unsafe:javascript:doEvilStuff()'); + + // ws chars before + testUrl = " \u000e javascript:doEvilStuff()"; + if (isEvilInCurrentBrowser(testUrl)) { + expect(sanitizeHref(testUrl)).toEqual('unsafe:javascript:doEvilStuff()'); + } + + // post-fixed with proper url + testUrl = "javascript:doEvilStuff(); http://make.me/look/good"; + expect(sanitizeHref(testUrl)).toBeOneOf( + 'unsafe:javascript:doEvilStuff(); http://make.me/look/good', + 'unsafe:javascript:doEvilStuff();%20http://make.me/look/good' + ); + })); + + + it('should sanitize ngHref bindings as well', inject(function() { + testUrl = "javascript:doEvilStuff()"; + expect(sanitizeHref(testUrl)).toBe('unsafe:javascript:doEvilStuff()'); + })); + + + it('should not sanitize valid urls', inject(function() { + testUrl = "foo/bar"; + expect(sanitizeHref(testUrl)).toBe('foo/bar'); + + testUrl = "/foo/bar"; + expect(sanitizeHref(testUrl)).toBe('/foo/bar'); + + testUrl = "../foo/bar"; + expect(sanitizeHref(testUrl)).toBe('../foo/bar'); + + testUrl = "#foo"; + expect(sanitizeHref(testUrl)).toBe('#foo'); + + testUrl = "http://foo/bar"; + expect(sanitizeHref(testUrl)).toBe('http://foo/bar'); + + testUrl = " http://foo/bar"; + expect(sanitizeHref(testUrl)).toBe(' http://foo/bar'); + + testUrl = "https://foo/bar"; + expect(sanitizeHref(testUrl)).toBe('https://foo/bar'); + + testUrl = "ftp://foo/bar"; + expect(sanitizeHref(testUrl)).toBe('ftp://foo/bar'); + + testUrl = "mailto:foo@bar.com"; + expect(sanitizeHref(testUrl)).toBe('mailto:foo@bar.com'); + + testUrl = "file:///foo/bar.html"; + expect(sanitizeHref(testUrl)).toBe('file:///foo/bar.html'); + })); + + it('should allow reconfiguration of the href whitelist', function() { + var returnVal; + expect(sanitizeUriProvider.aHrefSanitizationWhitelist() instanceof RegExp).toBe(true); + returnVal = sanitizeUriProvider.aHrefSanitizationWhitelist(/javascript:/); + expect(returnVal).toBe(sanitizeUriProvider); + + testUrl = "javascript:doEvilStuff()"; + expect(sanitizeHref(testUrl)).toBe('javascript:doEvilStuff()'); + + testUrl = "http://recon/figured"; + expect(sanitizeHref(testUrl)).toBe('unsafe:http://recon/figured'); + }); + + }); + +}); \ No newline at end of file diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js index 3d586830fee5..1958ec0f3e36 100644 --- a/test/ngSanitize/sanitizeSpec.js +++ b/test/ngSanitize/sanitizeSpec.js @@ -5,12 +5,15 @@ describe('HTML', function() { var expectHTML; beforeEach(module('ngSanitize')); - - beforeEach(inject(function($sanitize) { + beforeEach(function() { expectHTML = function(html){ - return expect($sanitize(html)); + var sanitize; + inject(function($sanitize) { + sanitize = $sanitize; + }); + return expect(sanitize(html)); }; - })); + }); describe('htmlParser', function() { if (angular.isUndefined(window.htmlParser)) return; @@ -183,13 +186,22 @@ describe('HTML', function() { toEqual(''); }); + it('should keep spaces as prefix/postfix', function() { + expectHTML(' a ').toEqual(' a '); + }); + + it('should allow multiline strings', function() { + expectHTML('\na\n').toEqual(' a\ '); + }); + describe('htmlSanitizerWriter', function() { if (angular.isUndefined(window.htmlSanitizeWriter)) return; - var writer, html; + var writer, html, uriValidator; beforeEach(function() { html = ''; - writer = htmlSanitizeWriter({push:function(text){html+=text;}}); + uriValidator = jasmine.createSpy('uriValidator'); + writer = htmlSanitizeWriter({push:function(text){html+=text;}}, uriValidator); }); it('should write basic HTML', function() { @@ -258,41 +270,106 @@ describe('HTML', function() { }); }); - describe('isUri', function() { + describe('uri validation', function() { + it('should call the uri validator', function() { + writer.start('a', {href:'someUrl'}, false); + expect(uriValidator).toHaveBeenCalledWith('someUrl', false); + uriValidator.reset(); + writer.start('img', {src:'someImgUrl'}, false); + expect(uriValidator).toHaveBeenCalledWith('someImgUrl', true); + uriValidator.reset(); + writer.start('someTag', {src:'someNonUrl'}, false); + expect(uriValidator).not.toHaveBeenCalled(); + }); - function isUri(value) { - return value.match(URI_REGEXP); - } + it('should drop non valid uri attributes', function() { + uriValidator.andReturn(false); + writer.start('a', {href:'someUrl'}, false); + expect(html).toEqual(''); - it('should be URI', function() { - expect(isUri('http://abc')).toBeTruthy(); - expect(isUri('HTTP://abc')).toBeTruthy(); - expect(isUri('https://abc')).toBeTruthy(); - expect(isUri('HTTPS://abc')).toBeTruthy(); - expect(isUri('ftp://abc')).toBeTruthy(); - expect(isUri('FTP://abc')).toBeTruthy(); - expect(isUri('mailto:me@example.com')).toBeTruthy(); - expect(isUri('MAILTO:me@example.com')).toBeTruthy(); - expect(isUri('tel:123-123-1234')).toBeTruthy(); - expect(isUri('TEL:123-123-1234')).toBeTruthy(); - expect(isUri('#anchor')).toBeTruthy(); + html = ''; + uriValidator.andReturn(true); + writer.start('a', {href:'someUrl'}, false); + expect(html).toEqual(''); }); + }); + }); - it('should not be URI', function() { - expect(isUri('')).toBeFalsy(); - expect(isUri('javascript:alert')).toBeFalsy(); + describe('uri checking', function() { + beforeEach(function() { + this.addMatchers({ + toBeValidUrl: function() { + var sanitize; + inject(function($sanitize) { + sanitize = $sanitize; + }); + var input = ''; + return sanitize(input) === input; + }, + toBeValidImageSrc: function() { + var sanitize; + inject(function($sanitize) { + sanitize = $sanitize; + }); + var input = ''; + return sanitize(input) === input; + } }); }); - describe('javascript URL attribute', function() { - beforeEach(function() { - this.addMatchers({ - toBeValidUrl: function() { - return URI_REGEXP.exec(this.actual); - } - }); + it('should use $$sanitizeUri for links', function() { + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); + module(function($provide) { + $provide.value('$$sanitizeUri', $$sanitizeUri); }); + inject(function() { + $$sanitizeUri.andReturn('someUri'); + expectHTML('').toEqual(''); + expect($$sanitizeUri).toHaveBeenCalledWith('someUri', false); + + $$sanitizeUri.andReturn('unsafe:someUri'); + expectHTML('').toEqual(''); + }); + }); + + it('should use $$sanitizeUri for links', function() { + var $$sanitizeUri = jasmine.createSpy('$$sanitizeUri'); + module(function($provide) { + $provide.value('$$sanitizeUri', $$sanitizeUri); + }); + inject(function() { + $$sanitizeUri.andReturn('someUri'); + + expectHTML('').toEqual(''); + expect($$sanitizeUri).toHaveBeenCalledWith('someUri', true); + + $$sanitizeUri.andReturn('unsafe:someUri'); + expectHTML('').toEqual(''); + }); + }); + + it('should be URI', function() { + expect('').toBeValidUrl(); + expect('http://abc').toBeValidUrl(); + expect('HTTP://abc').toBeValidUrl(); + expect('https://abc').toBeValidUrl(); + expect('HTTPS://abc').toBeValidUrl(); + expect('ftp://abc').toBeValidUrl(); + expect('FTP://abc').toBeValidUrl(); + expect('mailto:me@example.com').toBeValidUrl(); + expect('MAILTO:me@example.com').toBeValidUrl(); + expect('tel:123-123-1234').toBeValidUrl(); + expect('TEL:123-123-1234').toBeValidUrl(); + expect('#anchor').toBeValidUrl(); + expect('/page1.md').toBeValidUrl(); + }); + + it('should not be URI', function() { + expect('javascript:alert').not.toBeValidUrl(); + }); + + describe('javascript URLs', function() { it('should ignore javascript:', function() { expect('JavaScript:abc').not.toBeValidUrl(); expect(' \n Java\n Script:abc').not.toBeValidUrl(); @@ -318,15 +395,19 @@ describe('HTML', function() { }); it('should ignore hex encoded whitespace javascript:', function() { - expect('jav ascript:alert("A");').not.toBeValidUrl(); - expect('jav ascript:alert("B");').not.toBeValidUrl(); - expect('jav ascript:alert("C");').not.toBeValidUrl(); - expect('jav\u0000ascript:alert("D");').not.toBeValidUrl(); - expect('java\u0000\u0000script:alert("D");').not.toBeValidUrl(); - expect('  java\u0000\u0000script:alert("D");').not.toBeValidUrl(); + expect('jav ascript:alert();').not.toBeValidUrl(); + expect('jav ascript:alert();').not.toBeValidUrl(); + expect('jav ascript:alert();').not.toBeValidUrl(); + expect('jav\u0000ascript:alert();').not.toBeValidUrl(); + expect('java\u0000\u0000script:alert();').not.toBeValidUrl(); + expect('  java\u0000\u0000script:alert();').not.toBeValidUrl(); }); }); + }); - + describe('sanitizeText', function() { + it('should escape text', function() { + expect(sanitizeText('a
&
c')).toEqual('a<div>&</div>c'); + }); }); });