Skip to content

perf(core): make sanitization tree-shakable in Ivy mode #31934

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 13 commits into from

Conversation

mhevery
Copy link
Contributor

@mhevery mhevery commented Jul 31, 2019

In VE the Sanitizer is always available in BrowserModule because the VE retrieves it using injection.

In Ivy the injection is optional and we have instructions instead of component definition arrays. The implication of this is that in Ivy the instructions can pull in the sanitizer only when they are working with a property which is known to be unsafe. Because the Injection is optional this works even if no Sanitizer is present. So in Ivy we first use the sanitizer which is pulled in by the instruction, unless one is available through the Injector then we use that one instead.

This PR does few things:

  1. It makes Sanitizer optional in Ivy.
  2. It makes DomSanitizer tree shakable.
  3. It aligns the semantics of Ivy Sanitizer with that of the Ivy sanitization rules.
  4. It refactors DomSanitizer to use same functions as Ivy sanitization for consistency.

@mhevery mhevery requested review from a team as code owners July 31, 2019 18:17
Copy link
Contributor

@IgorMinar IgorMinar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to review this more carefully to have good feedback, so I'm just posting initial observations.

Three more things:

  • Can you please write up a good commit message describing the change and providing enough context for all the changes you made?
  • Can you take a look at this older PR that Martin has been trying to land for a while: #12310 and see if it makes sense to incorporate it into this change?
  • We will likely need Rafael or someone else from the security team to take a look at this as well as the changes you made are nontrivial.

@mhevery mhevery requested a review from a team as a code owner August 1, 2019 04:11
@mhevery mhevery force-pushed the angular_size branch 2 times, most recently from d7c631c to 8494709 Compare August 1, 2019 23:34
@mhevery mhevery added target: major This PR is targeted for the next major release area: core Issues related to the framework runtime labels Aug 3, 2019
@ngbot ngbot bot added this to the needsTriage milestone Aug 3, 2019
@mhevery mhevery force-pushed the angular_size branch 2 times, most recently from 9eebece to b9f0211 Compare August 8, 2019 22:23
Copy link
Contributor

@IgorMinar IgorMinar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few more comments / suggestions

*/
export interface TrustedString extends String { [BRAND]: BypassType; }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not remove anything, I just moved it from platform-browser to the core. We could add additional branding if you would like, but that is not what we had in the past.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see what you are doing... rather than use branded types you have switched to using real classes (e.g. SafeValueImpl and descendants).

But in that case I don't see any value in having both real implementations and interfaces... especially since the interfaces are all semantically identically now...

const x: SafeValue = new SafeHtmlImpl('some string');
const y: SafeScript = x;  // No compile time error...

Why not just make them all classes and ditch the interfaces?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be a breaking change.

export function allowSanitizationBypassAndThrow(value: any, type: BypassType): boolean;
export function allowSanitizationBypassAndThrow(value: any, type: BypassType): boolean {
const actualType = getSanitizationBypassType(value);
if (actualType != null && actualType !== type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (actualType != null && actualType !== type) {
if (actualType !== null && actualType !== type) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I actually want this to be sensitive to null and undefined

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least in the compiler we have been trying to be more explicit about these things, so if we wanted to compare both null and undefined then we would write it out explicitly using ===. This would ensure people like @IgorMinar don't mistake this sort of comparison as an error. I think the minimizer might be able to collapse such comparisons back to == at bundling time so there is no benefit in using it in the source.

Alternatively, I think a comment explaining why != is being used rather than !==` would help.

@@ -38,8 +38,8 @@ export function ɵɵsanitizeHtml(unsafeHtml: any): string {
if (sanitizer) {
return sanitizer.sanitize(SecurityContext.HTML, unsafeHtml) || '';
}
if (allowSanitizationBypass(unsafeHtml, BypassType.Html)) {
return unsafeHtml.toString();
if (allowSanitizationBypassAndThrow(unsafeHtml, BypassType.Html)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"andThrow"? what does that mean? you don't seem to be throwing anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the function throws if the sanitization is not valid. Can you suggest a different name?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the name, I feel that the problem is that this function is trying to do two different things:

  1. check whether we can bypass the sanitizer for these types
  2. check whether sanitization is allowed at all for these types

It seems to me that point 2) should be a check that the sanitizer should be doing e.g. in _sanitizeHtml and friends.

@mhevery mhevery requested a review from a team as a code owner August 9, 2019 18:55
@mhevery mhevery requested a review from a team as a code owner August 10, 2019 00:15
Copy link
Contributor

@petebacondarwin petebacondarwin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came to review for docs-infra but then found that you had removed all the changes in aio :-)

*/
export interface TrustedString extends String { [BRAND]: BypassType; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see what you are doing... rather than use branded types you have switched to using real classes (e.g. SafeValueImpl and descendants).

But in that case I don't see any value in having both real implementations and interfaces... especially since the interfaces are all semantically identically now...

const x: SafeValue = new SafeHtmlImpl('some string');
const y: SafeScript = x;  // No compile time error...

Why not just make them all classes and ditch the interfaces?

export function allowSanitizationBypassAndThrow(value: any, type: BypassType): boolean;
export function allowSanitizationBypassAndThrow(value: any, type: BypassType): boolean {
const actualType = getSanitizationBypassType(value);
if (actualType != null && actualType !== type) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least in the compiler we have been trying to be more explicit about these things, so if we wanted to compare both null and undefined then we would write it out explicitly using ===. This would ensure people like @IgorMinar don't mistake this sort of comparison as an error. I think the minimizer might be able to collapse such comparisons back to == at bundling time so there is no benefit in using it in the source.

Alternatively, I think a comment explaining why != is being used rather than !==` would help.

throw new Error(
`Required a safe ${type}, got a ${actualType} (see http://g.co/ng/security#xss)`);
}
return actualType === type;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I think this function will:

  • return true if
    • actualType === type
    • or actualType is ResourceUrl and type is Url
  • return false: if
    • actualType === null and type !== null
    • or actualType === undefined and type !== undefined
  • throw otherwise

Is that correct? It is not so obvious to glean that from reading the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have suggestions as to make it more clear?

@@ -38,8 +38,8 @@ export function ɵɵsanitizeHtml(unsafeHtml: any): string {
if (sanitizer) {
return sanitizer.sanitize(SecurityContext.HTML, unsafeHtml) || '';
}
if (allowSanitizationBypass(unsafeHtml, BypassType.Html)) {
return unsafeHtml.toString();
if (allowSanitizationBypassAndThrow(unsafeHtml, BypassType.Html)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding the name, I feel that the problem is that this function is trying to do two different things:

  1. check whether we can bypass the sanitizer for these types
  2. check whether sanitization is allowed at all for these types

It seems to me that point 2) should be a check that the sanitizer should be doing e.g. in _sanitizeHtml and friends.

ɵɵstylingApply();
});
expect(sanitizerInterceptor.lastValue).toEqual(null);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this test no longer applicable? If it is then should it appear somewhere in the new code?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this test is invalid, and has been removed.

@mhevery
Copy link
Contributor Author

mhevery commented Aug 13, 2019

@petebacondarwin The type confusion comes from the fact that I am moving code between packages. There is no change to public API. Yes your suggestions would be a great improvement but it would be breaking change as it would make it more strict and so it should be done in separate PR.

In VE the `Sanitizer` is always available in `BrowserModule` because the VE retrieves it using injection.

In Ivy the injection is optional and we have instructions instead of component definition arrays. The implication of this is that in Ivy the instructions can pull in the sanitizer only when they are working with a property which is known to be unsafe. Because the Injection is optional this works even if no Sanitizer is present. So in Ivy we first use the sanitizer which is pulled in by the instruction, unless one is available through the `Injector` then we use that one instead.

This PR does few things:
1) It makes `Sanitizer` optional in Ivy.
2) It makes `DomSanitizer` tree shakable.
3) It aligns the semantics of Ivy `Sanitizer` with that of the Ivy sanitization rules.
4) It refactors `DomSanitizer` to use same functions as Ivy sanitization for consistency.
@mhevery
Copy link
Contributor Author

mhevery commented Aug 13, 2019

presubmit

Copy link
Contributor

@IgorMinar IgorMinar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm, but please resolve Pete's comments.

@mhevery
Copy link
Contributor Author

mhevery commented Aug 13, 2019

presubmit

@mhevery mhevery added action: merge The PR is ready for merge by the caretaker merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note labels Aug 14, 2019
@mhevery
Copy link
Contributor Author

mhevery commented Aug 14, 2019

MERGE-ASSISTANCE: Unrelated failure (Unable to force green state)

@AndrewKushnir
Copy link
Contributor

The changes in this PR break some g3 targets (Guitar tests) with Ivy, adding "Blocked" label for now, so we can investigate problems before merging this PR.

@AndrewKushnir
Copy link
Contributor

Also started a Global TAP Presubmit to see if there are other affected targets.

@AndrewKushnir
Copy link
Contributor

AndrewKushnir commented Aug 14, 2019

@AndrewKushnir AndrewKushnir added action: presubmit The PR is in need of a google3 presubmit and removed merge: caretaker note Alert the caretaker performing the merge to check the PR for an out of normal action needed or note action: presubmit The PR is in need of a google3 presubmit state: blocked labels Aug 14, 2019
gkalpak added a commit to gkalpak/angular that referenced this pull request Aug 26, 2019
…TIZATION_PROVIDERS__POST_R3__`

The JSDoc tag was introduced in angular#31934 and was not intentional according
to [this discussion on Slack][1].

[1]: https://angular-team.slack.com/archives/CHB51S90D/p1566322373094100?thread_ts=1566292123.093500&cid=CHB51S90D
mhevery pushed a commit that referenced this pull request Aug 29, 2019
…TIZATION_PROVIDERS__POST_R3__` (#32314)

The JSDoc tag was introduced in #31934 and was not intentional according
to [this discussion on Slack][1].

[1]: https://angular-team.slack.com/archives/CHB51S90D/p1566322373094100?thread_ts=1566292123.093500&cid=CHB51S90D

PR Close #32314
sabeersulaiman pushed a commit to sabeersulaiman/angular that referenced this pull request Sep 6, 2019
In VE the `Sanitizer` is always available in `BrowserModule` because the VE retrieves it using injection.

In Ivy the injection is optional and we have instructions instead of component definition arrays. The implication of this is that in Ivy the instructions can pull in the sanitizer only when they are working with a property which is known to be unsafe. Because the Injection is optional this works even if no Sanitizer is present. So in Ivy we first use the sanitizer which is pulled in by the instruction, unless one is available through the `Injector` then we use that one instead.

This PR does few things:
1) It makes `Sanitizer` optional in Ivy.
2) It makes `DomSanitizer` tree shakable.
3) It aligns the semantics of Ivy `Sanitizer` with that of the Ivy sanitization rules.
4) It refactors `DomSanitizer` to use same functions as Ivy sanitization for consistency.

PR Close angular#31934
sabeersulaiman pushed a commit to sabeersulaiman/angular that referenced this pull request Sep 6, 2019
…TIZATION_PROVIDERS__POST_R3__` (angular#32314)

The JSDoc tag was introduced in angular#31934 and was not intentional according
to [this discussion on Slack][1].

[1]: https://angular-team.slack.com/archives/CHB51S90D/p1566322373094100?thread_ts=1566292123.093500&cid=CHB51S90D

PR Close angular#32314
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 15, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
action: merge The PR is ready for merge by the caretaker area: core Issues related to the framework runtime cla: yes target: major This PR is targeted for the next major release
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants