Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit b99d064

Browse files
committed
fix(core): parse URLs using the browser's DOM API
1 parent 715d97d commit b99d064

File tree

7 files changed

+160
-75
lines changed

7 files changed

+160
-75
lines changed

angularFiles.js

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ angularFiles = {
3030
'src/ng/httpBackend.js',
3131
'src/ng/locale.js',
3232
'src/ng/timeout.js',
33+
'src/ng/urlUtils.js',
3334

3435
'src/ng/filter.js',
3536
'src/ng/filter/filter.js',

src/AngularPublic.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@ function publishExternalAPI(angular){
125125
$sniffer: $SnifferProvider,
126126
$templateCache: $TemplateCacheProvider,
127127
$timeout: $TimeoutProvider,
128-
$window: $WindowProvider
128+
$window: $WindowProvider,
129+
$$urlUtils: $$UrlUtilsProvider
129130
});
130131
}
131132
]);

src/ng/compile.js

+12-13
Original file line numberDiff line numberDiff line change
@@ -274,9 +274,9 @@ function $CompileProvider($provide) {
274274

275275
this.$get = [
276276
'$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
277-
'$controller', '$rootScope', '$document',
277+
'$controller', '$rootScope', '$document', '$$urlUtils',
278278
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
279-
$controller, $rootScope, $document) {
279+
$controller, $rootScope, $document, $$urlUtils) {
280280

281281
var Attributes = function(element, attr) {
282282
this.$$element = element;
@@ -319,24 +319,23 @@ function $CompileProvider($provide) {
319319
}
320320
}
321321

322+
nodeName = nodeName_(this.$$element);
322323

323324
// sanitize a[href] and img[src] values
324-
nodeName = nodeName_(this.$$element);
325325
if ((nodeName === 'A' && key === 'href') ||
326-
(nodeName === 'IMG' && key === 'src')){
327-
urlSanitizationNode.setAttribute('href', value);
328-
329-
// href property always returns normalized absolute url, so we can match against that
330-
normalizedVal = urlSanitizationNode.href;
331-
if (normalizedVal !== '') {
332-
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
333-
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
334-
this[key] = value = 'unsafe:' + normalizedVal;
326+
(nodeName === 'IMG' && key === 'src')) {
327+
// NOTE: $$urlUtils.resolve() doesn't support IE < 8 so we don't sanitize for that case.
328+
if (!msie || msie >= 8 ) {
329+
normalizedVal = $$urlUtils.resolve(value);
330+
if (normalizedVal !== '') {
331+
if ((key === 'href' && !normalizedVal.match(aHrefSanitizationWhitelist)) ||
332+
(key === 'src' && !normalizedVal.match(imgSrcSanitizationWhitelist))) {
333+
this[key] = value = 'unsafe:' + normalizedVal;
334+
}
335335
}
336336
}
337337
}
338338

339-
340339
if (writeAttr !== false) {
341340
if (value === null || value === undefined) {
342341
this.$$element.removeAttr(attrName);

src/ng/http.js

+3-40
Original file line numberDiff line numberDiff line change
@@ -29,43 +29,6 @@ function parseHeaders(headers) {
2929
}
3030

3131

32-
var IS_SAME_DOMAIN_URL_MATCH = /^(([^:]+):)?\/\/(\w+:{0,1}\w*@)?([\w\.-]*)?(:([0-9]+))?(.*)$/;
33-
34-
35-
/**
36-
* Parse a request and location URL and determine whether this is a same-domain request.
37-
*
38-
* @param {string} requestUrl The url of the request.
39-
* @param {string} locationUrl The current browser location url.
40-
* @returns {boolean} Whether the request is for the same domain.
41-
*/
42-
function isSameDomain(requestUrl, locationUrl) {
43-
var match = IS_SAME_DOMAIN_URL_MATCH.exec(requestUrl);
44-
// if requestUrl is relative, the regex does not match.
45-
if (match == null) return true;
46-
47-
var domain1 = {
48-
protocol: match[2],
49-
host: match[4],
50-
port: int(match[6]) || DEFAULT_PORTS[match[2]] || null,
51-
// IE8 sets unmatched groups to '' instead of undefined.
52-
relativeProtocol: match[2] === undefined || match[2] === ''
53-
};
54-
55-
match = SERVER_MATCH.exec(locationUrl);
56-
var domain2 = {
57-
protocol: match[1],
58-
host: match[3],
59-
port: int(match[5]) || DEFAULT_PORTS[match[1]] || null
60-
};
61-
62-
return (domain1.protocol == domain2.protocol || domain1.relativeProtocol) &&
63-
domain1.host == domain2.host &&
64-
(domain1.port == domain2.port || (domain1.relativeProtocol &&
65-
domain2.port == DEFAULT_PORTS[domain2.protocol]));
66-
}
67-
68-
6932
/**
7033
* Returns a function that provides access to parsed headers.
7134
*
@@ -168,8 +131,8 @@ function $HttpProvider() {
168131
*/
169132
var responseInterceptorFactories = this.responseInterceptors = [];
170133

171-
this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector',
172-
function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector) {
134+
this.$get = ['$httpBackend', '$browser', '$cacheFactory', '$rootScope', '$q', '$injector', '$$urlUtils',
135+
function($httpBackend, $browser, $cacheFactory, $rootScope, $q, $injector, $$urlUtils) {
173136

174137
var defaultCache = $cacheFactory('$http');
175138

@@ -657,7 +620,7 @@ function $HttpProvider() {
657620
config.headers = headers;
658621
config.method = uppercase(config.method);
659622

660-
var xsrfValue = isSameDomain(config.url, $browser.url())
623+
var xsrfValue = $$urlUtils.isSameOrigin(config.url)
661624
? $browser.cookies()[config.xsrfCookieName || defaults.xsrfCookieName]
662625
: undefined;
663626
if (xsrfValue) {

src/ng/urlUtils.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict';
2+
3+
function $$UrlUtilsProvider() {
4+
this.$get = ['$window', '$document', function($window, $document) {
5+
var urlParsingNode = $document[0].createElement("a"),
6+
originUrl = resolve($window.location.href, true);
7+
8+
/**
9+
* @description
10+
* Normalizes and optionally parses a URL.
11+
*
12+
* NOTE: This is a private service. The API is subject to change unpredictably in any commit.
13+
*
14+
* Implementation Notes for non-IE browsers
15+
* ----------------------------------------
16+
* Assigning a URL to the href property of an anchor DOM node, even one attached to the DOM,
17+
* results both in the normalizing and parsing of the URL. Normalizing means that a relative
18+
* URL will be resolved into an absolute URL in the context of the application document.
19+
* Parsing means that the anchor node's host, hostname, protocol, port, pathname and related
20+
* properties are all populated to reflect the normalized URL. This approach has wide
21+
* compatibility - Safari 1+, Mozilla 1+, Opera 7+,e etc. See
22+
* http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
23+
*
24+
* Implementation Notes for IE
25+
* ---------------------------
26+
* IE >= 8 and <= 10 normalizes the URL when assigned to the anchor node similar to the other
27+
* browsers. However, the parsed components will not be set if the URL assigned did not specify
28+
* them. (e.g. if you assign a.href = "foo", then a.protocol, a.host, etc. will be empty.) We
29+
* work around that by performing the parsing in a 2nd step by taking a previously normalized
30+
* URL (e.g. by assining to a.href) and assigning it a.href again. This correctly populates the
31+
* properties such as protocol, hostname, port, etc.
32+
*
33+
* IE7 does not normalize the URL when assigned to an anchor node. (Apparently, it does, if one
34+
* uses the inner HTML approach to assign the URL as part of an HTML snippet -
35+
* http://stackoverflow.com/a/472729) However, setting img[src] does normalize the URL.
36+
* Unfortunately, setting img[src] to something like "javascript:foo" on IE throws an exception.
37+
* Since the primary usage for normalizing URLs is to sanitize such URLs, we can't use that
38+
* method and IE < 8 is unsupported.
39+
*
40+
* References:
41+
* http://developer.mozilla.org/en-US/docs/Web/API/HTMLAnchorElement
42+
* http://www.aptana.com/reference/html/api/HTMLAnchorElement.html
43+
* http://url.spec.whatwg.org/#urlutils
44+
* https://github.com/angular/angular.js/pull/2902
45+
* http://james.padolsey.com/javascript/parsing-urls-with-the-dom/
46+
*
47+
* @param {string} url The URL to be parsed.
48+
* @param {boolean=} parse When true, returns an object for the parsed URL. Otherwise, returns
49+
* a single string that is the normalized URL.
50+
* @returns {object|string} When parse is true, returns the normalized URL as a string.
51+
* Otherwise, returns an object with the following members.
52+
*
53+
* | member name | Description |
54+
* |===============|================|
55+
* | href | A normalized version of the provided URL if it was not an absolute URL |
56+
* | protocol | The protocol including the trailing colon |
57+
* | host | The host and port (if the port is non-default) of the normalizedUrl |
58+
*
59+
* These fields from the UrlUtils interface are currently not needed and hence not returned.
60+
*
61+
* | member name | Description |
62+
* |===============|================|
63+
* | hostname | The host without the port of the normalizedUrl |
64+
* | pathname | The path following the host in the normalizedUrl |
65+
* | hash | The URL hash if present |
66+
* | search | The query string |
67+
*
68+
*/
69+
function resolve(url, parse) {
70+
var href = url;
71+
if (msie) {
72+
// Normalize before parse. Refer Implementation Notes on why this is
73+
// done in two steps on IE.
74+
urlParsingNode.setAttribute("href", href);
75+
href = urlParsingNode.href;
76+
}
77+
urlParsingNode.setAttribute('href', href);
78+
79+
if (!parse) {
80+
return urlParsingNode.href;
81+
}
82+
// urlParsingNode provides the UrlUtils interface - http://url.spec.whatwg.org/#urlutils
83+
return {
84+
href: urlParsingNode.href,
85+
protocol: urlParsingNode.protocol,
86+
host: urlParsingNode.host
87+
// Currently unused and hence commented out.
88+
// hostname: urlParsingNode.hostname,
89+
// port: urlParsingNode.port,
90+
// pathname: urlParsingNode.pathname,
91+
// hash: urlParsingNode.hash,
92+
// search: urlParsingNode.search
93+
};
94+
}
95+
96+
return {
97+
resolve: resolve,
98+
/**
99+
* Parse a request URL and determine whether this is a same-origin request as the application document.
100+
*
101+
* @param {string} requestUrl The url of the request.
102+
* @returns {boolean} Whether the request is for the same origin as the application document.
103+
*/
104+
isSameOrigin: function isSameOrigin(requestUrl) {
105+
var parsed = resolve(requestUrl, true);
106+
return (parsed.protocol === originUrl.protocol &&
107+
parsed.host === originUrl.host);
108+
}
109+
};
110+
}];
111+
}

test/ng/httpSpec.js

-21
Original file line numberDiff line numberDiff line change
@@ -1476,25 +1476,4 @@ describe('$http', function() {
14761476

14771477
$httpBackend.verifyNoOutstandingExpectation = noop;
14781478
});
1479-
1480-
describe('isSameDomain', function() {
1481-
it('should support various combinations of urls', function() {
1482-
expect(isSameDomain('path/morepath',
1483-
'http://www.adomain.com')).toBe(true);
1484-
expect(isSameDomain('http://www.adomain.com/path',
1485-
'http://www.adomain.com')).toBe(true);
1486-
expect(isSameDomain('//www.adomain.com/path',
1487-
'http://www.adomain.com')).toBe(true);
1488-
expect(isSameDomain('//www.adomain.com/path',
1489-
'https://www.adomain.com')).toBe(true);
1490-
expect(isSameDomain('//www.adomain.com/path',
1491-
'http://www.adomain.com:1234')).toBe(false);
1492-
expect(isSameDomain('https://www.adomain.com/path',
1493-
'http://www.adomain.com')).toBe(false);
1494-
expect(isSameDomain('http://www.adomain.com:1234/path',
1495-
'http://www.adomain.com')).toBe(false);
1496-
expect(isSameDomain('http://www.anotherdomain.com/path',
1497-
'http://www.adomain.com')).toBe(false);
1498-
});
1499-
});
15001479
});

test/ng/urlUtilsSpec.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
'use strict';
2+
3+
describe('$$urlUtils', function() {
4+
describe('parse', function() {
5+
it('should normalize a relative url', inject(function($$urlUtils) {
6+
expect($$urlUtils.resolve("foo")).toMatch(/^https?:\/\/[^/]+\/foo$/);
7+
}));
8+
9+
it('should parse relative URL into component pieces', inject(function($$urlUtils) {
10+
var parsed = $$urlUtils.resolve("foo", true);
11+
expect(parsed.href).toMatch(/https?:\/\//);
12+
expect(parsed.protocol).toMatch(/^https?:/);
13+
expect(parsed.host).not.toBe("");
14+
}));
15+
});
16+
17+
describe('isSameOrigin', function() {
18+
it('should support various combinations of urls', inject(function($$urlUtils, $document) {
19+
expect($$urlUtils.isSameOrigin('path')).toBe(true);
20+
var origin = $$urlUtils.resolve($document[0].location.href, true);
21+
expect($$urlUtils.isSameOrigin('//' + origin.host + '/path')).toBe(true);
22+
// Different domain.
23+
expect($$urlUtils.isSameOrigin('http://example.com/path')).toBe(false);
24+
// Auto fill protocol.
25+
expect($$urlUtils.isSameOrigin('//example.com/path')).toBe(false);
26+
// Should not match when the ports are different.
27+
// This assumes that the test is *not* running on port 22 (very unlikely).
28+
expect($$urlUtils.isSameOrigin('//' + origin.hostname + ':22/path')).toBe(false);
29+
}));
30+
});
31+
});

0 commit comments

Comments
 (0)