diff options
| -rwxr-xr-x | angularFiles.js | 1 | ||||
| -rwxr-xr-x | src/AngularPublic.js | 3 | ||||
| -rw-r--r-- | src/ng/compile.js | 25 | ||||
| -rw-r--r-- | src/ng/http.js | 43 | ||||
| -rw-r--r-- | src/ng/urlUtils.js | 111 | ||||
| -rw-r--r-- | test/ng/httpSpec.js | 21 | ||||
| -rw-r--r-- | test/ng/urlUtilsSpec.js | 31 |
7 files changed, 160 insertions, 75 deletions
diff --git a/angularFiles.js b/angularFiles.js index a9d636b1..b93283a7 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -30,6 +30,7 @@ angularFiles = { 'src/ng/httpBackend.js', 'src/ng/locale.js', 'src/ng/timeout.js', + 'src/ng/urlUtils.js', 'src/ng/filter.js', 'src/ng/filter/filter.js', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 03d465ff..99f6cdbd 100755 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -125,7 +125,8 @@ function publishExternalAPI(angular){ $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, $timeout: $TimeoutProvider, - $window: $WindowProvider + $window: $WindowProvider, + $$urlUtils: $$UrlUtilsProvider }); } ]); diff --git a/src/ng/compile.js b/src/ng/compile.js index 7d2b6dc7..46ebe71a 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -274,9 +274,9 @@ function $CompileProvider($provide) { this.$get = [ '$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse', - '$controller', '$rootScope', '$document', + '$controller', '$rootScope', '$document', '$$urlUtils', function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse, - $controller, $rootScope, $document) { + $controller, $rootScope, $document, $$urlUtils) { var Attributes = function(element, attr) { this.$$element = element; @@ -319,24 +319,23 @@ function $CompileProvider($provide) { } } + nodeName = nodeName_(this.$$element); // sanitize a[href] and img[src] values - nodeName = nodeName_(this.$$element); if ((nodeName === 'A' && key === 'href') || - (nodeName === 'IMG' && key === 'src')){ - urlSanitizationNode.setAttribute('href', value); - - // href property always returns normalized absolute url, so we can match against that - normalizedVal = urlSanitizationNode.href; - if (normalizedVal !== '') { - if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || - (key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { - this[key] = value = 'unsafe:' + normalizedVal; + (nodeName === 'IMG' && key === 'src')) { + // NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case. + if (!msie || msie >= 8 ) { + normalizedVal = $$urlUtils.resolve(value); + if (normalizedVal !== '') { + if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) || + (key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) { + this[key] = value = 'unsafe:' + normalizedVal; + } } } } - if (writeAttr !== false) { if (value === null || value === undefined) { this.$$element.removeAttr(attrName); diff --git a/src/ng/http.js b/src/ng/http.js index a44da3a4..2aedeacb 100644 --- a/src/ng/http.js +++ b/src/ng/http.js @@ -29,43 +29,6 @@ function parseHeaders(headers) { } -var IS_SAME_DOMAIN_URL_MATCH = /^(([^:]+):)?\/\/(\w+:{0,1}\w*@)?([\w\.-]*)?(:([0-9]+))?(.*)$/; - - -/** - * Parse a request and location URL and determine whether this is a same-domain request. - * - * @param {string} requestUrl The url of the request. - * @param {string} locationUrl The current browser location url. - * @returns {boolean} Whether the request is for the same domain. - */ -function isSameDomain(requestUrl, locationUrl) { - var match = IS_SAME_DOMAIN_URL_MATCH.exec(requestUrl); - // if requestUrl is relative, the regex does not match. - if (match == null) return true; - - var domain1 = { - protocol: match[2], - host: match[4], - port: int(match[6]) || DEFAULT_PORTS[match[2]] || null, - // IE8 sets unmatched groups to '' instead of undefined. - relativeProtocol: match[2] === undefined || match[2] === '' - }; - - match = SERVER_MATCH.exec(locationUrl); - var domain2 = { - protocol: match[1], - host: match[3], - port: int(match[5]) || DEFAULT_PORTS[match[1]] || null - }; - - return (domain1.protocol == domain2.protocol || domain1.relativeProtocol) && - domain1.host == domain2.host && - (domain1.port == domain2.port || (domain1.relativeProtocol && - domain2.port == DEFAULT_PORTS[domain2.protocol])); -} - - /** * Returns a function that provides access to parsed headers. * @@ -168,8 +131,8 @@ function $HttpProvider() { */ var responseInterceptorFactories = this.responseInterceptors = []; - this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', - function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) { + this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', '$$urlUtils', + function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector, $$urlUtils) { var defaultCache = $cacheFactory('$http'); @@ -657,7 +620,7 @@ function $HttpProvider() { config.headers = headers; config.method = uppercase(config.method); - var xsrfValue = isSameDomain(config.url, $browser.url()) + var xsrfValue = $$urlUtils.isSameOrigin(config.url) ? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName] : undefined; if (xsrfValue) { diff --git a/src/ng/urlUtils.js b/src/ng/urlUtils.js new file mode 100644 index 00000000..e19f9860 --- /dev/null +++ b/src/ng/urlUtils.js @@ -0,0 +1,111 @@ +'use strict'; + +function $$UrlUtilsProvider() { + this.$get = ['$window', '$document', function($window, $document) { + var urlParsingNode = $document[0].createElement("a"), + originUrl = resolve($window.location.href, true); + + /** + * @description + * Normalizes and optionally parses a URL. + * + * NOTE: This is a private service. The API is subject to change unpredictably in any commit. + * + * Implementation Notes for non-IE browsers + * ---------------------------------------- + * Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM, + * results both in the normalizing and parsing of the URL. Normalizing means that a relative + * URL will be resolved into an absolute URL in the context of the application document. + * Parsing means that the anchor node's host, hostname, protocol, port, pathname and related + * properties are all populated to reflect the normalized URL. This approach has wide + * compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * + * Implementation Notes for IE + * --------------------------- + * IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other + * browsers. However, the parsed components will not be set if the URL assigned did not specify + * them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We + * work around that by performing the parsing in a 2nd step by taking a previously normalized + * URL (e.g. by assining to a.href) and assigning it a.href again. This correctly populates the + * properties such as protocol, hostname, port, etc. + * + * IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one + * uses the inner HTML approach to assign the URL as part of an HTML snippet - + * http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL. + * Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception. + * Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that + * method and IE < 8 is unsupported. + * + * References: + * http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement + * http://www.aptana.com/reference/html/api/HTMLAnchorElement.html + * http://url.spec.whatwg.org/#urlutils + * https://github.com/angular/angular.js/pull/2902 + * http://james.padolsey.com/javascript/parsing-urls-with-the-dom/ + * + * @param {string} url The URL to be parsed. + * @param {boolean=} parse When true, returns an object for the parsed URL. Otherwise, returns + * a single string that is the normalized URL. + * @returns {object|string} When parse is true, returns the normalized URL as a string. + * Otherwise, returns an object with the following members. + * + * | member name | Description | + * |===============|================| + * | href | A normalized version of the provided URL if it was not an absolute URL | + * | protocol | The protocol including the trailing colon | + * | host | The host and port (if the port is non-default) of the normalizedUrl | + * + * These fields from the UrlUtils interface are currently not needed and hence not returned. + * + * | member name | Description | + * |===============|================| + * | hostname | The host without the port of the normalizedUrl | + * | pathname | The path following the host in the normalizedUrl | + * | hash | The URL hash if present | + * | search | The query string | + * + */ + function resolve(url, parse) { + var href = url; + if (msie) { + // Normalize before parse. Refer Implementation Notes on why this is + // done in two steps on IE. + urlParsingNode.setAttribute("href", href); + href = urlParsingNode.href; + } + urlParsingNode.setAttribute('href', href); + + if (!parse) { + return urlParsingNode.href; + } + // urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils + return { + href: urlParsingNode.href, + protocol: urlParsingNode.protocol, + host: urlParsingNode.host + // Currently unused and hence commented out. + // hostname: urlParsingNode.hostname, + // port: urlParsingNode.port, + // pathname: urlParsingNode.pathname, + // hash: urlParsingNode.hash, + // search: urlParsingNode.search + }; + } + + return { + resolve: resolve, + /** + * Parse a request URL and determine whether this is a same-origin request as the application document. + * + * @param {string} requestUrl The url of the request. + * @returns {boolean} Whether the request is for the same origin as the application document. + */ + isSameOrigin: function isSameOrigin(requestUrl) { + var parsed = resolve(requestUrl, true); + return (parsed.protocol === originUrl.protocol && + parsed.host === originUrl.host); + } + }; + }]; +} diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js index cefc88e1..ec1cb7f1 100644 --- a/test/ng/httpSpec.js +++ b/test/ng/httpSpec.js @@ -1476,25 +1476,4 @@ describe('$http', function() { $httpBackend.verifyNoOutstandingExpectation = noop; }); - - describe('isSameDomain', function() { - it('should support various combinations of urls', function() { - expect(isSameDomain('path/morepath', - 'http://www.adomain.com')).toBe(true); - expect(isSameDomain('http://www.adomain.com/path', - 'http://www.adomain.com')).toBe(true); - expect(isSameDomain('//www.adomain.com/path', - 'http://www.adomain.com')).toBe(true); - expect(isSameDomain('//www.adomain.com/path', - 'https://www.adomain.com')).toBe(true); - expect(isSameDomain('//www.adomain.com/path', - 'http://www.adomain.com:1234')).toBe(false); - expect(isSameDomain('https://www.adomain.com/path', - 'http://www.adomain.com')).toBe(false); - expect(isSameDomain('http://www.adomain.com:1234/path', - 'http://www.adomain.com')).toBe(false); - expect(isSameDomain('http://www.anotherdomain.com/path', - 'http://www.adomain.com')).toBe(false); - }); - }); }); diff --git a/test/ng/urlUtilsSpec.js b/test/ng/urlUtilsSpec.js new file mode 100644 index 00000000..57043a5a --- /dev/null +++ b/test/ng/urlUtilsSpec.js @@ -0,0 +1,31 @@ +'use strict'; + +describe('$$urlUtils', function() { + describe('parse', function() { + it('should normalize a relative url', inject(function($$urlUtils) { + expect($$urlUtils.resolve("foo")).toMatch(/^https?:\/\/[^/]+\/foo$/); + })); + + it('should parse relative URL into component pieces', inject(function($$urlUtils) { + var parsed = $$urlUtils.resolve("foo", true); + expect(parsed.href).toMatch(/https?:\/\//); + expect(parsed.protocol).toMatch(/^https?:/); + expect(parsed.host).not.toBe(""); + })); + }); + + describe('isSameOrigin', function() { + it('should support various combinations of urls', inject(function($$urlUtils, $document) { + expect($$urlUtils.isSameOrigin('path')).toBe(true); + var origin = $$urlUtils.resolve($document[0].location.href, true); + expect($$urlUtils.isSameOrigin('//' + origin.host + '/path')).toBe(true); + // Different domain. + expect($$urlUtils.isSameOrigin('http://example.com/path')).toBe(false); + // Auto fill protocol. + expect($$urlUtils.isSameOrigin('//example.com/path')).toBe(false); + // Should not match when the ports are different. + // This assumes that the test is *not* running on port 22 (very unlikely). + expect($$urlUtils.isSameOrigin('//' + origin.hostname + ':22/path')).toBe(false); + })); + }); +}); |
