diff options
| -rwxr-xr-x | angularFiles.js | 1 | ||||
| -rw-r--r-- | src/AngularPublic.js | 5 | ||||
| -rw-r--r-- | src/ng/compile.js | 34 | ||||
| -rw-r--r-- | src/ng/sanitizeUri.js | 74 | ||||
| -rw-r--r-- | src/ngSanitize/filter/linky.js | 44 | ||||
| -rw-r--r-- | src/ngSanitize/sanitize.js | 46 | ||||
| -rwxr-xr-x | test/ng/compileSpec.js | 296 | ||||
| -rw-r--r-- | test/ng/sanitizeUriSpec.js | 230 | ||||
| -rw-r--r-- | test/ngSanitize/sanitizeSpec.js | 159 | 
9 files changed, 550 insertions, 339 deletions
| diff --git a/angularFiles.js b/angularFiles.js index 9aa2ef7e..ad968341 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 eb97b4c5..d2c325c5 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 13fb9682..54d2dc9f 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 00000000..97325094 --- /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 39494f2d..2c05d84e 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 @@       </doc:scenario>     </doc:example>   */ -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('<a '); +      if (angular.isDefined(target)) { +        html.push('target="'); +        html.push(target); +        html.push('" '); +      } +      html.push('href="'); +      html.push(url); +      html.push('">'); +      addText(text); +      html.push('</a>'); +    }    }; -}); +}]); diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js index 7bd9aae3..5d378b02 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');     </doc:scenario>     </doc:example>   */ -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 = /<!DOCTYPE([^>]*?)>/i,    CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/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(/</g,"<"); -  return hiddenPre.innerText || hiddenPre.textContent || ''; +  if (!value) { +    return ''; +  } +  // Note: IE8 does not preserve spaces at the start/end of innerHTML +  var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/; +  var parts = spaceRe.exec(value); +  parts[0] = ''; +  if (parts[2]) { +    hiddenPre.innerHTML=parts[2].replace(/</g,"<"); +    parts[2] = hiddenPre.innerText || hiddenPre.textContent; +  } +  return parts.join('');  }  /** @@ -384,7 +408,7 @@ function encodeEntities(value) {   *     comment: function(text) {}   * }   */ -function htmlSanitizeWriter(buf){ +function htmlSanitizeWriter(buf, uriValidator){    var ignore = false;    var out = angular.bind(buf, buf.push);    return { @@ -398,7 +422,9 @@ function htmlSanitizeWriter(buf){          out(tag);          angular.forEach(attrs, function(value, key){            var lkey=angular.lowercase(key); -          if (validAttrs[lkey]===true && (uriAttrs[lkey]!==true || value.match(URI_REGEXP))) { +          var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background'); +          if (validAttrs[lkey] === true && +            (uriAttrs[lkey] !== true || uriValidator(value, isImage))) {              out(' ');              out(key);              out('="'); @@ -430,4 +456,4 @@ function htmlSanitizeWriter(buf){  // define ngSanitize module and register $sanitize service -angular.module('ngSanitize', []).value('$sanitize', $sanitize); +angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider); diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js index 80788b02..6de96f65 100755 --- a/test/ng/compileSpec.js +++ b/test/ng/compileSpec.js @@ -3834,6 +3834,7 @@ describe('$compile', function() {    describe('img[src] sanitization', function() { +      it('should NOT require trusted values for img src', inject(function($rootScope, $compile, $sce) {        element = $compile('<img src="{{testUrl}}"></img>')($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('<img src="{{testUrl}}"></a>')($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('<img src="{{testUrl}}"></a>')($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('<img src="{{dataUri}}"></img>')($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('<img src="{{testUrl}}"></a>')($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('<img src="{{testUrl}}"></img>')($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('<img ng-src="{{testUrl}}"></img>')($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('<img src="{{testUrl}}"></img>')($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('<img title="{{testUrl}}"></img>')($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('<img src="{{testUrl}}"></img>')($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('<a href="{{testUrl}}"></a>')($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('<a href="{{testUrl}}"></a>')($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('<a href="{{testUrl}}"></a>')($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('<a ng-href="{{testUrl}}"></a>')($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('<a href="{{testUrl}}"></a>')($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('<div href="{{testUrl}}"></div>')($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('<a title="{{testUrl}}"></a>')($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('<a href="{{testUrl}}"></a>')($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 00000000..b9f6a0e2 --- /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 3d586830..1958ec0f 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('<a>'); -      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('<a href="someUrl">');        }); +    }); +  }); -      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 = '<a href="'+this.actual+'"></a>'; +          return sanitize(input) === input; +        }, +        toBeValidImageSrc: function() { +          var sanitize; +          inject(function($sanitize) { +            sanitize = $sanitize; +          }); +          var input = '<img src="'+this.actual+'"/>'; +          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('<a href="someUri"></a>').toEqual('<a href="someUri"></a>'); +        expect($$sanitizeUri).toHaveBeenCalledWith('someUri', false); + +        $$sanitizeUri.andReturn('unsafe:someUri'); +        expectHTML('<a href="someUri"></a>').toEqual('<a></a>'); +      }); +    }); + +    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('<img src="someUri"/>').toEqual('<img src="someUri"/>'); +        expect($$sanitizeUri).toHaveBeenCalledWith('someUri', true); + +        $$sanitizeUri.andReturn('unsafe:someUri'); +        expectHTML('<img src="someUri"/>').toEqual('<img/>'); +      }); +    }); + +    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<div>&</div>c')).toEqual('a<div>&</div>c'); +    });    });  }); | 
