-
Notifications
You must be signed in to change notification settings - Fork 25k
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
fix(core): use appropriate inert document strategy for Firefox & Safari #17019
Conversation
abc704a
to
5c0104d
Compare
Locally I am getting this warning:
I am not sure what is causing this. |
5c0104d
to
bef4d33
Compare
it('should not allow JavaScript execution when creating inert document', () => { | ||
sanitizeHtml(defaultDoc, '<svg><g onload="window.xxx = 100"></g></svg>'); | ||
expect((window as any).xxx).toBe(undefined); | ||
delete (window as any).xxx; |
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.
Please don't do delete
it will make window a slow object for the rest of the tests. Just assign 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.
OK!
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.
Although that is quite an ironic statement given how slow the tests are generally :-P
The unit tests are failing on Travis. It seems it is because the |
bef4d33
to
3c1adac
Compare
/CC @rjamet |
@@ -121,53 +95,54 @@ class SanitizingHtmlSerializer { | |||
// because characters were re-encoded. | |||
public sanitizedSomething = false; | |||
private buf: string[] = []; | |||
private DOM = getDOM(); |
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: should be lowerCamelCase
, i.e. dom
, right? It doesn't have to be a field in the class instance btw, it should be an initialized-once module level field. We just need to take care to initialize after the initial angular load, otherwise we risk getting the wrong DOM.
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 was copying this casing from the previous code: https://github.com/angular/angular/pull/17019/files/3c1adac41b8a17262ae4c2dde7b61370869244f4#diff-4fba2f404f1048cbc43f747e3bc2606dL18. Happy to change this if appropriate.
I was not sure about the benefit of using a module level variable. Throughout the rest of the Angular code base, this getDOM()
is used in a variety of ways: as it is needed (not cached at all) - e.g. the Title
service; cached per instance - e.g. Meta
service; and cached per module. But the majority use the call as required approach and looking at the implementation of this method it is super simple method:
export function getDOM() {
return _DOM;
}
The downside of caching is that we get the wrong version of the DOM so it seems to me that it is actually safer and easier (and no slower) to just call getDOM()
as required.
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.
Indeed, we don't really have a system here. I think I'd either access it through the getter (which hopefully your optimizer should inline) or from a lazily initialized module variable. YMMV, and I don't have data pointing either way.
|
||
/** | ||
* Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to | ||
* the DOM in a browser environment. | ||
*/ | ||
export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string { | ||
const DOM = getDOM(); |
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.
initialize DOM
as a module level field here, as you do with inertBodyHelper
below
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 only reason I use a module level variable for inertBodyHelper
is that it is used in a free standing exported function (rather than a class) and so there was no other container to hold the cached object.
Also, inertBodyHelper
creates DOM elements and so it more performance sensitive than the simple implementation of getDOM()
.
|
||
import {DomAdapter, getDOM} from '../dom/dom_adapter'; | ||
|
||
export class InertBodyHelper { |
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.
add a comment on what this does?
import {DomAdapter, getDOM} from '../dom/dom_adapter'; | ||
|
||
export class InertBodyHelper { | ||
private inertDocument = this.DOM.createHtmlDocument(); |
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.
you should document the lifetime of this object, i.e. that we reuse the same document for many sanitization runs has performance implications (creating many documents would be expensive).
|
||
export class InertBodyHelper { | ||
private inertDocument = this.DOM.createHtmlDocument(); | ||
private inertBodyElement = this.DOM.querySelector(this.inertDocument, 'body'); |
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't you access inertDocument.body
?
this.inertBodyElement.innerHTML = | ||
'<svg><p><style><img src="</style><img src=x onerror=alert(1)//">'; | ||
if (this.inertBodyElement.querySelector('svg img')) { | ||
this.getInertBodyElement = this.getInertBodyElement_DOMParser; |
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 document which of these cases is the workaround for the FF bug?
// Check for the Safari 10.1 bug - which allows JS to run inside the SVG G element | ||
this.inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>'; | ||
if (!this.inertBodyElement.querySelector('svg')) { | ||
this.getInertBodyElement = this.getInertBodyElement_XHR; |
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 document that this is the Safari workaround case?
if (this.inertBodyElement.querySelector('svg img')) { | ||
this.getInertBodyElement = this.getInertBodyElement_DOMParser; | ||
} else { | ||
this.getInertBodyElement = this.getInertBodyElement_InertDocument; |
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.
document that this is the default and sane way.
} | ||
|
||
/** | ||
* Use createHtmlDocument to create and fill an inert body element |
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 code below doesn't use createHtmlDocument, fix the docs?
@@ -134,6 +134,22 @@ export function main() { | |||
} | |||
}); | |||
|
|||
// See | |||
// https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449 | |||
it('should not allow JavaScript execution when creating inert document', () => { |
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.
For my information, are we actually running tests in Safari?
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 security-wise. |
3c1adac
to
8691451
Compare
@mprobst I added another commit with changes based on your review. |
8691451
to
b0a1f13
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.
LGTM.
@@ -121,53 +95,54 @@ class SanitizingHtmlSerializer { | |||
// because characters were re-encoded. | |||
public sanitizedSomething = false; | |||
private buf: string[] = []; | |||
private DOM = getDOM(); |
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.
Indeed, we don't really have a system here. I think I'd either access it through the getter (which hopefully your optimizer should inline) or from a lazily initialized module variable. YMMV, and I don't have data pointing either way.
Just got to work out why it doesn't look good to Travis :-( |
You can preview 5f1556c at https://pr17019-5f1556c.ngbuilds.io/. |
5f1556c
to
1cdc1a9
Compare
@@ -3,7 +3,7 @@ | |||
"master": { | |||
"uncompressed": { | |||
"inline": 1447, | |||
"main": 151639, | |||
"main": 154185, |
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.
Unfortunately, it appears these changes affect the size of the CLI bundle due to the increase in the browser-platform
package.
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.
@mhevery / @IgorMinar who needs to OK this?
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.
approved
You can preview 1cdc1a9 at https://pr17019-1cdc1a9.ngbuilds.io/. |
1cdc1a9
to
69dbd01
Compare
You can preview 69dbd01 at https://pr17019-69dbd01.ngbuilds.io/. |
Both Firefox and Safari are vulnerable to XSS if we use an inert document created via `document.implementation.createHTMLDocument()`. Now we check for those vulnerabilities and then use a DOMParser or XHR strategy if needed. Further the platform-server has its own library for parsing HTML, so we sniff for that (by checking whether DOMParser exists) and fall back to the standard strategy. Thanks to @cure53 for the heads up on this issue.
69dbd01
to
eebc3c1
Compare
You can preview eebc3c1 at https://pr17019-eebc3c1.ngbuilds.io/. |
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.
OMG - 3kb?!?!? this makes me really sad. I suspect that we'll need to redo this in the future. lgtm for now.
@petebacondarwin can you please send a PR against the LTS branch as well? I believe 4.4.x |
LTS version here: #22077 |
…ri (#17019) Both Firefox and Safari are vulnerable to XSS if we use an inert document created via `document.implementation.createHTMLDocument()`. Now we check for those vulnerabilities and then use a DOMParser or XHR strategy if needed. Further the platform-server has its own library for parsing HTML, so we sniff for that (by checking whether DOMParser exists) and fall back to the standard strategy. Thanks to @cure53 for the heads up on this issue. PR Close #17019
…ri (angular#17019) Both Firefox and Safari are vulnerable to XSS if we use an inert document created via `document.implementation.createHTMLDocument()`. Now we check for those vulnerabilities and then use a DOMParser or XHR strategy if needed. Further the platform-server has its own library for parsing HTML, so we sniff for that (by checking whether DOMParser exists) and fall back to the standard strategy. Thanks to @cure53 for the heads up on this issue. PR Close angular#17019
…ri (angular#17019) Both Firefox and Safari are vulnerable to XSS if we use an inert document created via `document.implementation.createHTMLDocument()`. Now we check for those vulnerabilities and then use a DOMParser or XHR strategy if needed. Further the platform-server has its own library for parsing HTML, so we sniff for that (by checking whether DOMParser exists) and fall back to the standard strategy. Thanks to @cure53 for the heads up on this issue. PR Close angular#17019
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. |
Both Firefox and Safari are vulnerable to XSS if we use an inert document
created via
document.implementation.createHTMLDocument()
.Now we check for those vulnerabilities and then use a DOMParser or XHR
strategy if needed.
Thanks to @cure53 for the heads up on this issue.