fix(core): ensure sanitizer works if DOMParser return null body (#40107)

In some browsers, notably a mobile version of webkit on iPad, the
result of calling `DOMParser.parseFromString()` returns a document
whose `body` property is null until the next tick of the browser.
Since this is of no use to us for sanitization, we now fall back to the
"inert document" strategy for this case.

Fixes #39834

PR Close #40107
This commit is contained in:
Pete Bacon Darwin 2020-12-14 16:03:31 +00:00 committed by Joey Perrott
parent 8dbd2204c3
commit 8ebac24b48
2 changed files with 19 additions and 1 deletions

View File

@ -16,7 +16,8 @@ import {trustedHTMLFromString} from '../util/security/trusted_types';
* Fallback: InertDocument strategy
*/
export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
const inertDocumentHelper = new InertDocumentHelper(defaultDoc);
return isDOMParserAvailable() ? new DOMParserHelper(inertDocumentHelper) : inertDocumentHelper;
}
export interface InertBodyHelper {
@ -31,6 +32,8 @@ export interface InertBodyHelper {
* This is the default strategy used in browsers that support it.
*/
class DOMParserHelper implements InertBodyHelper {
constructor(private inertDocumentHelper: InertBodyHelper) {}
getInertBodyElement(html: string): HTMLElement|null {
// We add these extra elements to ensure that the rest of the content is parsed as expected
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
@ -41,6 +44,12 @@ class DOMParserHelper implements InertBodyHelper {
const body = new window.DOMParser()
.parseFromString(trustedHTMLFromString(html) as string, 'text/html')
.body as HTMLBodyElement;
if (body === null) {
// In some browsers (e.g. Mozilla/5.0 iPad AppleWebKit Mobile) the `body` property only
// becomes available in the following tick of the JS engine. In that case we fall back to
// the `inertDocumentHelper` instead.
return this.inertDocumentHelper.getInertBodyElement(html);
}
body.removeChild(body.firstChild!);
return body;
} catch {

View File

@ -252,5 +252,14 @@ function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
.toMatch(/<a href="unsafe:(&#12288;)?javascript:alert\(1\)">CLICKME<\/a>/);
});
}
if (isDOMParserAvailable()) {
it('should work even if DOMParser returns a null body', () => {
// Simulate `DOMParser.parseFromString()` returning a null body.
// See https://github.com/angular/angular/issues/39834
spyOn(window.DOMParser.prototype, 'parseFromString').and.returnValue({body: null} as any);
expect(sanitizeHtml(defaultDoc, 'Hello, World')).toEqual('Hello, World');
});
}
});
}