Skip to content
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

Closed

Conversation

petebacondarwin
Copy link
Member

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.

@petebacondarwin petebacondarwin added area: core Issues related to the framework runtime area: security Issues related to built-in security features, such as HTML sanitation action: review The PR is still awaiting reviews from at least one requested reviewer security Issues that generally impact framework or application security type: bug/fix labels May 25, 2017
@petebacondarwin
Copy link
Member Author

Locally I am getting this warning:

LOG: 'Attempting to configure '__esModule' with descriptor '{"value":true}' on object 'undefined' and got error, giving up: TypeError: Object.defineProperty called on non-object'

I am not sure what is causing this.

@mary-poppins
Copy link

The angular.io preview for abc704a is available here.

@mary-poppins
Copy link

The angular.io preview for 5c0104d is available here.

@mary-poppins
Copy link

The angular.io preview for bef4d33 is available here.

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;
Copy link
Contributor

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

Copy link
Member Author

Choose a reason for hiding this comment

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

OK!

Copy link
Member Author

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

@petebacondarwin
Copy link
Member Author

The unit tests are failing on Travis. It seems it is because the inertBodyElement does not have the querySelector method. Is this some different mode than I am running locally?

@mary-poppins
Copy link

The angular.io preview for 3c1adac is available here.

@mprobst
Copy link
Contributor

mprobst commented May 29, 2017

/CC @rjamet

@@ -121,53 +95,54 @@ class SanitizingHtmlSerializer {
// because characters were re-encoded.
public sanitizedSomething = false;
private buf: string[] = [];
private DOM = getDOM();
Copy link
Contributor

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.

Copy link
Member Author

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.

Copy link
Contributor

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();
Copy link
Contributor

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

Copy link
Member Author

@petebacondarwin petebacondarwin May 29, 2017

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 {
Copy link
Contributor

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();
Copy link
Contributor

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');
Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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;
Copy link
Contributor

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
Copy link
Contributor

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', () => {
Copy link
Contributor

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?

Copy link
Member Author

Choose a reason for hiding this comment

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

@koto
Copy link
Contributor

koto commented May 29, 2017

LGTM security-wise.

@petebacondarwin
Copy link
Member Author

@mprobst I added another commit with changes based on your review.

@mary-poppins
Copy link

The angular.io preview for 8691451 is available here.

@mary-poppins
Copy link

The angular.io preview for b0a1f13 is available here.

Copy link
Contributor

@mprobst mprobst left a 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();
Copy link
Contributor

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.

@petebacondarwin
Copy link
Member Author

Just got to work out why it doesn't look good to Travis :-(

@mary-poppins
Copy link

You can preview 5f1556c at https://pr17019-5f1556c.ngbuilds.io/.

@@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"inline": 1447,
"main": 151639,
"main": 154185,
Copy link
Member Author

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.

Copy link
Member Author

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?

Copy link
Contributor

Choose a reason for hiding this comment

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

approved

@mary-poppins
Copy link

You can preview 1cdc1a9 at https://pr17019-1cdc1a9.ngbuilds.io/.

@mary-poppins
Copy link

You can preview 69dbd01 at https://pr17019-69dbd01.ngbuilds.io/.

@petebacondarwin petebacondarwin added action: review The PR is still awaiting reviews from at least one requested reviewer and removed action: cleanup The PR is in need of cleanup, either due to needing a rebase or in response to comments from reviews labels Jan 31, 2018
@petebacondarwin petebacondarwin added action: merge The PR is ready for merge by the caretaker and removed action: review The PR is still awaiting reviews from at least one requested reviewer labels Jan 31, 2018
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.
@mary-poppins
Copy link

You can preview eebc3c1 at https://pr17019-eebc3c1.ngbuilds.io/.

@mhevery
Copy link
Contributor

mhevery commented Feb 7, 2018

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.

OMG - 3kb?!?!? this makes me really sad. I suspect that we'll need to redo this in the future. lgtm for now.

@IgorMinar
Copy link
Contributor

@petebacondarwin can you please send a PR against the LTS branch as well? I believe 4.4.x

@IgorMinar IgorMinar removed the target: lts This PR is targeting a version currently in long-term support label Feb 7, 2018
@petebacondarwin
Copy link
Member Author

LTS version here: #22077

@mhevery mhevery closed this in a751649 Feb 8, 2018
mhevery pushed a commit that referenced this pull request Feb 8, 2018
…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
jbogarthyde pushed a commit to jbogarthyde/angular that referenced this pull request Feb 23, 2018
…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
@petebacondarwin petebacondarwin deleted the sanitize-inert-doc branch February 25, 2018 10:00
leo6104 pushed a commit to leo6104/angular that referenced this pull request Mar 25, 2018
…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
@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 13, 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 area: security Issues related to built-in security features, such as HTML sanitation cla: yes security Issues that generally impact framework or application security target: patch This PR is targeted for the next patch release type: bug/fix
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet