-
Notifications
You must be signed in to change notification settings - Fork 27.4k
fix($sce): Consider document base URL for 'self' URL policy. #15145
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good in general. I have left a couple of minor (or not so minor) comments.
I think we need to account for base.href
chaning dynamically.
And there should probably be some unit tests too.
* or a parsed URL object. | ||
* @returns {boolean} Whether the URL is same-origin as the document base URL. | ||
*/ | ||
function urlIsSameOriginAsBaseUrl(requestUrl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I would rather not duplicate the code. Maybe have a base function that urlIsSameOrigin
/urlIsSameOriginAsBase
can delegate two (and pass the originUrl
/baseUrl
as parameters).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
* @returns {boolean} Whether the URL is same-origin as the document base URL. | ||
*/ | ||
function urlIsSameOriginAsBaseUrl(requestUrl) { | ||
if (!baseUrl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if base.href
changes dynamically ? I think we need to account for that too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is a good point. We can do this relatively painlessly by working directly with the anchor tag, since it keeps in sync with the base URL.
|
||
urlIsTrusted = browser.executeScript('return isTrustedUrl("/relative")'); | ||
expect(urlIsTrusted).toBe(true); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a test for absolute URL that matches base.href
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Please take another look. I refactored the code to use the the anchor tag more directly. Since it's always in always in sync with the base URL, the code now keeps up with updates to the base tag. |
It turned out the code was not working due to a bug in Firefox so I had to refactor again. When the base changed, the .href property on the anchor tag would get updated, but .host would retain its old value. I think the current implementation is very reasonable, though. |
Hey gkalpak, can you take another look? Let me know if there are any outstanding issues. |
/** | ||
* Parse a request URL and determine whether it is same-origin as the current document base URL. | ||
* | ||
* Note: The base URL is usually the same the document location (`location.href`) but can |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can --> can be
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the same the document location --> the same as ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
* @returns {boolean} Whether the URL is same-origin as the document base URL. | ||
*/ | ||
function urlIsSameOriginAsBaseUrl(requestUrl) { | ||
var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not move this line into urlsAreSameOrigin
as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
* @return {boolean} True if both URLs have the same origin, and false otherwise. | ||
*/ | ||
function urlsAreSameOrigin(url1, url2) { | ||
// IE sometimes includes a port in the 'host' property, even if it is the default 80 port so |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is that a problem? Both URLs should be consistent (either contain the port or don't).
Under what circumstances can the URLs differ in that regard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverting. You are right. This may have been more of an issue in a prior iteration of the code. If I recall correctly, the issue appeared in IE after the base tag had been changed dynamically. After this had occurred, urlParsingNode.host started returning hosts with ports, like www.example.com:80.
it('tracks changes to the base URL', function() { | ||
browser.executeScript( | ||
'document.getElementsByTagName("base")[0].href = "http://xxx.example.com/";'); | ||
checkUrl('http://xxx.example.com/path/to/file.html', true); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you also add checkUrl('http://www.example.com/path/to/file.html', false);
(just in case 😃)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. I think it's okay to just add it, but let me know if it should be in a separate it() block.
} | ||
|
||
beforeAll(function() { | ||
loadFixture('base_tag'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you change all names to kebab-case (to be consistent with the rest of the tests)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
closed/re-opened pull request to get Travis CI to rebuild. Looks like there was some connection error to SauceLabs that caused the CI build to fail |
I added some tests for urlIsSameOriginAsBaseUrl() in test/ng/urlUtilsSpec.js. I'm not quite sure how to test non-default base URL behavior in the unit test. We generally need to change the global document base URL to fully test things, but it seems sub-optimal to do so from a unit test. I originally put tests in an end-to-end test where we have more control over the page to avoid this kind of issue. So I think all the the tests are there, just kind of spread out. |
gkalpak, can you advise on how to test inside the unit tests? Or just LGTM if you think it's okay as is :-) |
@adob, I was planning to look into it, but I haven't found the time yet. We are focusing on getting the 1.6.0 release out, so we prioritize related tasks and issues/PRs. Once 1.6.0 is out, I will come back to this. (I am not even sure if it possible to add these tests in a reasonable way. I'll have to look into it. Feel free to leave the PR in its current state and I'll take it from there.) Thx for all the work so far btw 👍 |
OK. Thank you |
gkalpak, can you take a look at this? Is there anything I should add? |
I would still like to see a test in |
…licy. Page authors can use the base tag in HTML to specify URL to use as a base when resovling relative URLs. This can cause SCE to start rejecting relative URLs on the page because the fail the same-origin test. To improve compatiblity with the base tag, this commit changes the logic for matching urls to 'self' to allow URLs that match the protocol and domain of the base URL in addition to URLs that match the loading origin. Security note: If an attacker can inject a base tag into the page, they can circumvent SCE protections. Howerver, injecting a base tag typically requires the ability to inject arbitrary HTML into the page, which is a more serious vulnerabilty than bypassing SCE.
Added unit tests to test/ng/sceSpecs.js. Please take a look. |
/** | ||
* Parse a request URL and determine whether it is same-origin as the current document base URL. | ||
* | ||
* Note: The base URL is usually the same the document location (`location.href`) but can |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the same the document location --> the same as ...
afterAll(function() { | ||
window.document.head.removeChild(baseElem); | ||
}); | ||
function expectAllowed($sce, url) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This helper (and expectBlocked
below) seem redundant in this case. Let's remove them and inline the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
var baseElem = window.document.createElement('BASE'); | ||
var cfg = {whitelist: ['self'], blacklist: []}; | ||
baseElem.setAttribute('href', window.location.protocol + '//foo.example.com/path/'); | ||
beforeAll(function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is no particular reason, I would prefer re-creating/adding/removing the <base>
element for each test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
LGTM too ! |
Page authors can use the `<base>` tag in HTML to specify URL to use as a base when resovling relative URLs. This can cause SCE to reject relative URLs on the page, because they fail the same-origin test. To improve compatibility with the `<base>` tag, this commit changes the logic for matching URLs to the 'self' policy to allow URLs that match the protocol and domain of the base URL in addition to URLs that match the loading origin. **Security Note:** If an attacker can inject a `<base>` tag into the page, they can circumvent SCE protections. However, injecting a `<base>` tag typically requires the ability to inject arbitrary HTML into the page, which is a more serious vulnerabilty than bypassing SCE. Fixes #15144 Closes #15145
Page authors can use the `<base>` tag in HTML to specify URL to use as a base when resovling relative URLs. This can cause SCE to reject relative URLs on the page, because they fail the same-origin test. To improve compatibility with the `<base>` tag, this commit changes the logic for matching URLs to the 'self' policy to allow URLs that match the protocol and domain of the base URL in addition to URLs that match the loading origin. **Security Note:** If an attacker can inject a `<base>` tag into the page, they can circumvent SCE protections. However, injecting a `<base>` tag typically requires the ability to inject arbitrary HTML into the page, which is a more serious vulnerabilty than bypassing SCE. Fixes #15144 Closes #15145
(cc @xtofian and @adob for more pairs of eyes: bindings on base href don't have a context currently in AngularJS. This PR adds baseURI's origin to the URLs allowed by the 'self' whitelist for RESOURCE_URLs, and #15597 bumps the context of base href bindings to RESOURCE_URL. Do you think this PR is okay even without the added context on base href, or did I miss anything?) I assumed that was okay, but it's more complex than I thought, you're right to rollback :/ sorry I didn't figure that out before. As far as i can tell, there's only three relevant cases: an app on
The only caveat I can think of there is if the app lives on On the plus side, that will remove the need for a lot of trustAsResourceUrl calls. They're seen as the only way to solve I lean towards saying it's fine. The more concerning stuff with this PR without the other only happens when binding on base href, which is a bad idea in the first place. Otherwise, I don't see regressions, only quirkish behavior in unusual cases. |
Quick tests for the order of things when setting base href in various way: https://plnkr.co/edit/R54ihAo4q9gIlV6QPlye?p=preview for base href with no interpolation More quirks :/ |
I can think of some very extreme scenarios, like:
I understand this is a very contrived example, but still 😃 So, here is where we are currently:
I think it is safest to keep things that way (and not merge this PR into v1.6.x without #15597). |
I agree with your analysis. We could argue on whether these PRs make the app less safe than it was before, but overall, I'm siding with you: both PRs at the same time would be the safest. |
…attributes Previously, `base[href]` didn't have an SCE context. This was not ideal, because `base[href]` affects the behavior of all relative URLs across the page. Furthermore, since #15145, `base[href]` also affects which URLs are considered trusted under the 'self' policy for white- or black-listed resource URLs. This commit tightens the security of Angular apps, by adding `base[href]` to the list of RESOURCE_URL context attributes, essentially putting the same constraints on bindings to `base[href]` as on iframe or script sources. Refer to the [`$sce` API docs](https://code.angularjs.org/snapshot/docs/api/ng/service/$sce) for more info on SCE trusted contexts. Closes #15597 BREAKING CHANGE: Previously, `<base href="{{ $ctrl.baseUrl }}" />` would not require `baseUrl` to be trusted as a RESOURCE_URL. Now, `baseUrl` will be sent to `$sce`'s RESOURCE_URL checks. By default, it will break unless `baseUrl` is of the same origin as the application document. Refer to the [`$sce` API docs](https://code.angularjs.org/snapshot/docs/api/ng/service/$sce) for more info on how to trust a value in a RESOURCE_URL context. Also, concatenation in trusted contexts is not allowed, which means that the following won't work: `<base href="/something/{{ $ctrl.partialPath }}" />`. Either construct complex values in a controller (recommended): ```js this.baseUrl = '/something/' + this.partialPath; ``` ```html <base href="{{ $ctrl.baseUrl }}" /> ``` Or use string concatenation in the interpolation expression (not recommended except for the simplest of cases): ```html <base href="{{ '/something/' + $ctrl.partialPath }}" /> ```
Page authors can use the `<base>` tag in HTML to specify URL to use as a base when resovling relative URLs. This can cause SCE to reject relative URLs on the page, because they fail the same-origin test. To improve compatibility with the `<base>` tag, this commit changes the logic for matching URLs to the 'self' policy to allow URLs that match the protocol and domain of the base URL in addition to URLs that match the loading origin. **Security Note:** If an attacker can inject a `<base>` tag into the page, they can circumvent SCE protections. However, injecting a `<base>` tag typically requires the ability to inject arbitrary HTML into the page, which is a more serious vulnerabilty than bypassing SCE. Fixes angular#15144 Closes angular#15145
…attributes Previously, `base[href]` didn't have an SCE context. This was not ideal, because `base[href]` affects the behavior of all relative URLs across the page. Furthermore, since angular#15145, `base[href]` also affects which URLs are considered trusted under the 'self' policy for white- or black-listed resource URLs. This commit tightens the security of Angular apps, by adding `base[href]` to the list of RESOURCE_URL context attributes, essentially putting the same constraints on bindings to `base[href]` as on iframe or script sources. Refer to the [`$sce` API docs](https://code.angularjs.org/snapshot/docs/api/ng/service/$sce) for more info on SCE trusted contexts. Closes angular#15597 BREAKING CHANGE: Previously, `<base href="{{ $ctrl.baseUrl }}" />` would not require `baseUrl` to be trusted as a RESOURCE_URL. Now, `baseUrl` will be sent to `$sce`'s RESOURCE_URL checks. By default, it will break unless `baseUrl` is of the same origin as the application document. Refer to the [`$sce` API docs](https://code.angularjs.org/snapshot/docs/api/ng/service/$sce) for more info on how to trust a value in a RESOURCE_URL context. Also, concatenation in trusted contexts is not allowed, which means that the following won't work: `<base href="/something/{{ $ctrl.partialPath }}" />`. Either construct complex values in a controller (recommended): ```js this.baseUrl = '/something/' + this.partialPath; ``` ```html <base href="{{ $ctrl.baseUrl }}" /> ``` Or use string concatenation in the interpolation expression (not recommended except for the simplest of cases): ```html <base href="{{ '/something/' + $ctrl.partialPath }}" /> ```
This PR fixes the issue in bug #15144. If the page specifies a base URL using
<base>
tag, Angular may start to reject relative resource URL if the base URL domain does not match the loading domain. This PR adjusts matchUrl() to also consider the base URL (document.baseURI
) in addition tolocation.href
when matching URLs against the 'self' URl policy.This PR does not introduce any breaking changes.
Security note: SCE policy can be circumvented by rogue
<base>
tags on the page. However, I believe this is not too much of a concern. If an attacker can inject<base>
tags, then it's very likely they can inject arbitrary HTML, which is a way worse vulnerability.