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('