diff --git a/packages/core/src/sanitization/html_sanitizer.ts b/packages/core/src/sanitization/html_sanitizer.ts
index eb39f31a13..c6c9a89cae 100644
--- a/packages/core/src/sanitization/html_sanitizer.ts
+++ b/packages/core/src/sanitization/html_sanitizer.ts
@@ -83,6 +83,13 @@ const HTML_ATTRS = tagSet(
export const VALID_ATTRS = merge(URI_ATTRS, SRCSET_ATTRS, HTML_ATTRS);
+// Elements whose content should not be traversed/preserved, if the elements themselves are invalid.
+//
+// Typically, `Some content` would traverse (and in this case preserve)
+// `Some content`, but strip `invalid-element` opening/closing tags. For some elements, though, we
+// don't want to preserve the content, if the elements themselves are going to be removed.
+const SKIP_TRAVERSING_CONTENT_IF_INVALID_ELEMENTS = tagSet('script,style,template');
+
/**
* SanitizingHtmlSerializer serializes a DOM fragment, stripping out any unsafe elements and unsafe
* attributes.
@@ -98,17 +105,17 @@ class SanitizingHtmlSerializer {
// However this code never accesses properties off of `document` before deleting its contents
// again, so it shouldn't be vulnerable to DOM clobbering.
let current: Node = el.firstChild !;
- let elementValid = true;
+ let traverseContent = true;
while (current) {
if (current.nodeType === Node.ELEMENT_NODE) {
- elementValid = this.startElement(current as Element);
+ traverseContent = this.startElement(current as Element);
} else if (current.nodeType === Node.TEXT_NODE) {
this.chars(current.nodeValue !);
} else {
// Strip non-element, non-text nodes.
this.sanitizedSomething = true;
}
- if (elementValid && current.firstChild) {
+ if (traverseContent && current.firstChild) {
current = current.firstChild !;
continue;
}
@@ -132,18 +139,18 @@ class SanitizingHtmlSerializer {
}
/**
- * Outputs only valid Elements.
+ * Sanitizes an opening element tag (if valid) and returns whether the element's contents should
+ * be traversed. Element content must always be traversed (even if the element itself is not
+ * valid/safe), unless the element is one of `SKIP_TRAVERSING_CONTENT_IF_INVALID_ELEMENTS`.
*
- * Invalid elements are skipped.
- *
- * @param element element to sanitize
- * Returns true if the element is valid.
+ * @param element The element to sanitize.
+ * @return True if the element's contents should be traversed.
*/
private startElement(element: Element): boolean {
const tagName = element.nodeName.toLowerCase();
if (!VALID_ELEMENTS.hasOwnProperty(tagName)) {
this.sanitizedSomething = true;
- return false;
+ return !SKIP_TRAVERSING_CONTENT_IF_INVALID_ELEMENTS.hasOwnProperty(tagName);
}
this.buf.push('<');
this.buf.push(tagName);
diff --git a/packages/core/test/sanitization/html_sanitizer_spec.ts b/packages/core/test/sanitization/html_sanitizer_spec.ts
index 5f64b0abee..8d114f10a0 100644
--- a/packages/core/test/sanitization/html_sanitizer_spec.ts
+++ b/packages/core/test/sanitization/html_sanitizer_spec.ts
@@ -36,8 +36,8 @@ import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
.toEqual('
Hello
World
');
});
- it('supports removal of namespaced elements',
- () => { expect(_sanitizeHtml(defaultDoc, 'abc')).toEqual('a'); });
+ it('supports namespaced elements',
+ () => { expect(_sanitizeHtml(defaultDoc, 'abc')).toEqual('abc'); });
it('supports namespaced attributes', () => {
expect(_sanitizeHtml(defaultDoc, 't'))
@@ -85,7 +85,7 @@ import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
.toEqual('Hello
'); // NB: quote encoded as ASCII ".
});
- describe('should strip dangerous elements and its content', () => {
+ describe('should strip dangerous elements (but potentially traverse their content)', () => {
const dangerousTags = [
'form',
'object',
@@ -93,32 +93,40 @@ import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
'button',
'option',
'select',
- 'script',
- 'style',
];
-
for (const tag of dangerousTags) {
- it(`${tag}`,
- () => { expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!${tag}>`)).toEqual(''); });
+ it(tag,
+ () => { expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!${tag}>`)).toEqual('evil!'); });
}
+
const dangerousSelfClosingTags = [
- 'frameset',
- 'embed',
- 'input',
- 'param',
'base',
'basefont',
- 'param',
+ 'embed',
+ 'frameset',
+ 'input',
'link',
+ 'param',
];
-
for (const tag of dangerousSelfClosingTags) {
- it(`${tag}`, () => {
+ it(tag, () => {
expect(_sanitizeHtml(defaultDoc, `before<${tag}>After`)).toEqual('beforeAfter');
});
}
- it(`swallows frame entirely`, () => {
+ const dangerousSkipContentTags = [
+ 'script',
+ 'style',
+ 'template',
+ ];
+ for (const tag of dangerousSkipContentTags) {
+ it(tag, () => { expect(_sanitizeHtml(defaultDoc, `<${tag}>evil!${tag}>`)).toEqual(''); });
+ }
+
+ it(`frame`, () => {
+ // `` is special, because different browsers treat it differently (e.g. remove it
+ // altogether). // We just verify that (one way or another), there is no `` element
+ // after sanitization.
expect(_sanitizeHtml(defaultDoc, `evil!`)).not.toContain('');
});
});
@@ -133,6 +141,13 @@ import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
}
});
+ it('ignores content of script elements', () => {
+ expect(_sanitizeHtml(defaultDoc, '')).toEqual('');
+ expect(_sanitizeHtml(defaultDoc, 'hi
'))
+ .toEqual('hi
');
+ expect(_sanitizeHtml(defaultDoc, '')).toEqual('');
+ });
+
it('ignores content of style elements', () => {
expect(_sanitizeHtml(defaultDoc, 'hi
'))
.toEqual('hi
');
@@ -186,7 +201,7 @@ import {_sanitizeHtml} from '../../src/sanitization/html_sanitizer';
// PlatformBrowser output
'
' :
// PlatformServer output
- '');
+ '');
});
if (browserDetection.isWebkit) {