-
Notifications
You must be signed in to change notification settings - Fork 26.5k
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
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.
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.
d7c631c
to
8494709
Compare
9eebece
to
b9f0211
Compare
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.
a few more comments / suggestions
*/ | ||
export interface TrustedString extends String { [BRAND]: BypassType; } |
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 did you remove the branded interface? without it the meaning of these interfaces is lost because due to structural typing you can't ensure type safety: http://www.typescriptlang.org/play/#code/KYDwDg9gTgLgBASwHY2FAZgQwMbDgZU3WADVMAbAVzwG8BfAKAdElkRTS1wKOAAkYAW3JxQqJABMAzj2JkqtRs3DR4yVBhx5CxfDACe5PGOCSZO0hWpx6TFqvYau23vmxQEYeCbOzLCmyUGOEwAIykYKBx4bHJMKXNeeWoASUEwEQR0o0FTGES5K1pguGwIJAioSmwYaAAKMEpQ8gRsUoALTCQAc2RugBV2hCkAIShgTABrKQBBMAzWzBgEcvxgbEoPAwAuOEq+gEobErg4AHoz0XSDEqVTkPDI6Lhu4Bh+-TBgADlMXLqDrt9j0ANxMe61PQeHoA473ODjGCbJBwAAGFmSeEElAicBxeAA2mAoBAvrB9ABdAC8oWQEj6uwAJDQYEMpAA6bCdHp9QbDMYTaZzBbYJYrJBrDZbfR0VFwADUJ3haLgdSkwDw7RgMDA2wu3U5EDOPTO6qlCAMAGIQAkDqiwadGHdYvECvwhOQ0hlRCBxNI-JivZlssBcig3QJhHCXm8Pl9fv8jjQEW9kXAAOQAZnTILgToYLoSfj0hmAQZ9frdgeyiBDYfyxYMRmjr3enx+f2AsOTiLT6YALDm8wwlPY2OhKEgauK8UgAO5RMAYop1ABuRV2y4USZKvagKLXRRCVaKQYOnO5vR6fNG4yms3mLVFy1W602Fv0YKUZQq8BAcCpOAkGAOc-EjT1sjqdMpHTA4wQYCcpxfFFUAiOp0E3Vwm2AHd4T3FF0C-JgLjgVlhlI9suXWaY4HKch9DgUJ1niFxiHAkJJEbUsQnGPZImqJEono0j2jwKROwLcpcS1YRMLYj0ANI4A0JAOCGCAA
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.
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.
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.
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?
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 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) { |
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 (actualType != null && actualType !== type) { | |
if (actualType !== null && actualType !== type) { |
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.
No, I actually want this to be sensitive to null
and undefined
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.
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)) { |
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.
"andThrow"? what does that mean? you don't seem to be throwing anywhere.
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 function throws if the sanitization is not valid. Can you suggest a different name?
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.
Regarding the name, I feel that the problem is that this function is trying to do two different things:
- check whether we can bypass the sanitizer for these types
- 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.
packages/platform-browser/src/security/dom_sanitization_service.ts
Outdated
Show resolved
Hide resolved
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.
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; } |
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.
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) { |
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.
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; |
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.
So I think this function will:
- return
true
ifactualType === type
- or
actualType
isResourceUrl
andtype
isUrl
- return
false
: ifactualType === null
andtype !== null
- or
actualType === undefined
andtype !== undefined
- throw otherwise
Is that correct? It is not so obvious to glean that from reading 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.
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)) { |
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.
Regarding the name, I feel that the problem is that this function is trying to do two different things:
- check whether we can bypass the sanitizer for these types
- 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); | ||
}); |
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.
Is this test no longer applicable? If it is then should it appear somewhere in the new 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.
yes, this test is invalid, and has been removed.
@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.
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, but please resolve Pete's comments.
MERGE-ASSISTANCE: Unrelated failure (Unable to force green state) |
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. |
Also started a Global TAP Presubmit to see if there are other affected targets. |
…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
…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
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
…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
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
In VE the
Sanitizer
is always available inBrowserModule
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:
Sanitizer
optional in Ivy.DomSanitizer
tree shakable.Sanitizer
with that of the Ivy sanitization rules.DomSanitizer
to use same functions as Ivy sanitization for consistency.