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.
This commit is contained in:
Tobias Bosch 2013-11-25 15:40:18 -08:00
parent 68ceb17272
commit 333523483f
9 changed files with 561 additions and 350 deletions

1
angularFiles.js vendored
View file

@ -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',

View file

@ -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,

View file

@ -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) {

74
src/ng/sanitizeUri.js Normal file
View file

@ -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;
};
};
}

View file

@ -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>');
}
};
});
}]);

View file

@ -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,"&lt;");
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,"&lt;");
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);

View file

@ -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($compile, $rootScope) {
element = $compile('<img src="{{testUrl}}"></img>')($rootScope);
// 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";
$rootScope.$apply();
expect(element.attr('src')).toBe('unsafe:http://recon/figured');
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";
$$sanitizeUri.andReturn('someSanitizedUrl');
$rootScope.$apply();
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($compile, $rootScope) {
element = $compile('<a href="{{testUrl}}"></a>')($rootScope);
$rootScope.testUrl = "javascript:doEvilStuff()";
$rootScope.$apply();
expect(element.attr('href')).toBe('javascript:doEvilStuff()');
$rootScope.testUrl = "http://recon/figured";
$rootScope.$apply();
expect(element.attr('href')).toBe('unsafe:http://recon/figured');
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";
$$sanitizeUri.andReturn('someSanitizedUrl');
$rootScope.$apply();
expect(element.attr('href')).toBe('someSanitizedUrl');
expect($$sanitizeUri).toHaveBeenCalledWith($rootScope.testUrl, false);
});
});
});
describe('interpolation on HTML DOM event handler attributes onclick, onXYZ, formaction', function() {

230
test/ng/sanitizeUriSpec.js Normal file
View file

@ -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');
});
});
});

View file

@ -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('&#10;a\&#10;');
});
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() {
function isUri(value) {
return value.match(URI_REGEXP);
}
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();
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();
});
it('should not be URI', function() {
expect(isUri('')).toBeFalsy();
expect(isUri('javascript:alert')).toBeFalsy();
it('should drop non valid uri attributes', function() {
uriValidator.andReturn(false);
writer.start('a', {href:'someUrl'}, false);
expect(html).toEqual('<a>');
html = '';
uriValidator.andReturn(true);
writer.start('a', {href:'someUrl'}, false);
expect(html).toEqual('<a href="someUrl">');
});
});
});
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&#x09;ascript:alert("A");').not.toBeValidUrl();
expect('jav&#x0A;ascript:alert("B");').not.toBeValidUrl();
expect('jav&#x0A ascript:alert("C");').not.toBeValidUrl();
expect('jav\u0000ascript:alert("D");').not.toBeValidUrl();
expect('java\u0000\u0000script:alert("D");').not.toBeValidUrl();
expect(' &#14; java\u0000\u0000script:alert("D");').not.toBeValidUrl();
expect('jav&#x09;ascript:alert();').not.toBeValidUrl();
expect('jav&#x0A;ascript:alert();').not.toBeValidUrl();
expect('jav&#x0A ascript:alert();').not.toBeValidUrl();
expect('jav\u0000ascript:alert();').not.toBeValidUrl();
expect('java\u0000\u0000script:alert();').not.toBeValidUrl();
expect(' &#14; java\u0000\u0000script:alert();').not.toBeValidUrl();
});
});
});
describe('sanitizeText', function() {
it('should escape text', function() {
expect(sanitizeText('a<div>&</div>c')).toEqual('a&lt;div&gt;&amp;&lt;/div&gt;c');
});
});
});