diff options
| author | Tobias Bosch | 2013-11-25 15:40:18 -0800 | 
|---|---|---|
| committer | Tobias Bosch | 2013-11-26 14:29:38 -0800 | 
| commit | 333523483f3ce6dd3177b697a5e5a7177ca364c8 (patch) | |
| tree | 34cd700540680a7d9276cd1fe231b6f72fd232ae /src | |
| parent | 68ceb17272bdd2ebc838565070973d93704f4427 (diff) | |
| download | angular.js-333523483f3ce6dd3177b697a5e5a7177ca364c8.tar.bz2 | |
fix($sanitize): Use same whitelist mechanism as $compile does.
`$sanitize` now uses the same mechanism as `$compile` to validate uris.
By this, the validation in `$sanitize` is more general and can be
configured in the same way as the one in `$compile`.
Changes
- Creates the new private service `$$sanitizeUri`.
- Moves related specs from `compileSpec.js` into `sanitizeUriSpec.js`.
- Refactors the `linky` filter to be less dependent on `$sanitize`
  internal functions.
Fixes #3748.
Diffstat (limited to 'src')
| -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 | 
5 files changed, 155 insertions, 48 deletions
| 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); | 
