
Provides a runtime and compile time switch for ivy including `ApplicationRef.bootstrapModule`. This is done by naming the symbols such that `ngcc` (angular Compatibility compiler) can rename symbols in such a way that running `ngcc` command will switch the `@angular/core` module from `legacy` to `ivy` mode. This is done as follows: ``` const someToken__PRE_NGCC__ = ‘legacy mode’; const someToken__POST_NGCC__ = ‘ivy mode’; export someSymbol = someToken__PRE_NGCC__; ``` The `ngcc` will search for any token which ends with `__PRE_NGCC__` and replace it with `__POST_NGCC__`. This allows the `@angular/core` package to be rewritten to ivy mode post `ngcc` execution. PR Close #25238
266 lines
9.8 KiB
TypeScript
266 lines
9.8 KiB
TypeScript
/**
|
||
* @license
|
||
* Copyright Google Inc. All Rights Reserved.
|
||
*
|
||
* Use of this source code is governed by an MIT-style license that can be
|
||
* found in the LICENSE file at https://angular.io/license
|
||
*/
|
||
|
||
import {isDevMode} from '../is_dev_mode';
|
||
import {InertBodyHelper} from './inert_body';
|
||
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
|
||
|
||
function tagSet(tags: string): {[k: string]: boolean} {
|
||
const res: {[k: string]: boolean} = {};
|
||
for (const t of tags.split(',')) res[t] = true;
|
||
return res;
|
||
}
|
||
|
||
function merge(...sets: {[k: string]: boolean}[]): {[k: string]: boolean} {
|
||
const res: {[k: string]: boolean} = {};
|
||
for (const s of sets) {
|
||
for (const v in s) {
|
||
if (s.hasOwnProperty(v)) res[v] = true;
|
||
}
|
||
}
|
||
return res;
|
||
}
|
||
|
||
// Good source of info about elements and attributes
|
||
// http://dev.w3.org/html5/spec/Overview.html#semantics
|
||
// http://simon.html5.org/html-elements
|
||
|
||
// Safe Void Elements - HTML5
|
||
// http://dev.w3.org/html5/spec/Overview.html#void-elements
|
||
const VOID_ELEMENTS = tagSet('area,br,col,hr,img,wbr');
|
||
|
||
// Elements that you can, intentionally, leave open (and which close themselves)
|
||
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
|
||
const OPTIONAL_END_TAG_BLOCK_ELEMENTS = tagSet('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr');
|
||
const OPTIONAL_END_TAG_INLINE_ELEMENTS = tagSet('rp,rt');
|
||
const OPTIONAL_END_TAG_ELEMENTS =
|
||
merge(OPTIONAL_END_TAG_INLINE_ELEMENTS, OPTIONAL_END_TAG_BLOCK_ELEMENTS);
|
||
|
||
// Safe Block Elements - HTML5
|
||
const BLOCK_ELEMENTS = merge(
|
||
OPTIONAL_END_TAG_BLOCK_ELEMENTS,
|
||
tagSet(
|
||
'address,article,' +
|
||
'aside,blockquote,caption,center,del,details,dialog,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' +
|
||
'h6,header,hgroup,hr,ins,main,map,menu,nav,ol,pre,section,summary,table,ul'));
|
||
|
||
// Inline Elements - HTML5
|
||
const INLINE_ELEMENTS = merge(
|
||
OPTIONAL_END_TAG_INLINE_ELEMENTS,
|
||
tagSet(
|
||
'a,abbr,acronym,audio,b,' +
|
||
'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,picture,q,ruby,rp,rt,s,' +
|
||
'samp,small,source,span,strike,strong,sub,sup,time,track,tt,u,var,video'));
|
||
|
||
const VALID_ELEMENTS =
|
||
merge(VOID_ELEMENTS, BLOCK_ELEMENTS, INLINE_ELEMENTS, OPTIONAL_END_TAG_ELEMENTS);
|
||
|
||
// Attributes that have href and hence need to be sanitized
|
||
const URI_ATTRS = tagSet('background,cite,href,itemtype,longdesc,poster,src,xlink:href');
|
||
|
||
// Attributes that have special href set hence need to be sanitized
|
||
const SRCSET_ATTRS = tagSet('srcset');
|
||
|
||
const HTML_ATTRS = tagSet(
|
||
'abbr,accesskey,align,alt,autoplay,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,' +
|
||
'compact,controls,coords,datetime,default,dir,download,face,headers,height,hidden,hreflang,hspace,' +
|
||
'ismap,itemscope,itemprop,kind,label,lang,language,loop,media,muted,nohref,nowrap,open,preload,rel,rev,role,rows,rowspan,rules,' +
|
||
'scope,scrolling,shape,size,sizes,span,srclang,start,summary,tabindex,target,title,translate,type,usemap,' +
|
||
'valign,value,vspace,width');
|
||
|
||
// NB: This currently consciously doesn't support SVG. SVG sanitization has had several security
|
||
// issues in the past, so it seems safer to leave it out if possible. If support for binding SVG via
|
||
// innerHTML is required, SVG attributes should be added here.
|
||
|
||
// NB: Sanitization does not allow <form> elements or other active elements (<button> etc). Those
|
||
// can be sanitized, but they increase security surface area without a legitimate use case, so they
|
||
// are left out here.
|
||
|
||
const VALID_ATTRS = merge(URI_ATTRS, SRCSET_ATTRS, HTML_ATTRS);
|
||
|
||
/**
|
||
* SanitizingHtmlSerializer serializes a DOM fragment, stripping out any unsafe elements and unsafe
|
||
* attributes.
|
||
*/
|
||
class SanitizingHtmlSerializer {
|
||
// Explicitly track if something was stripped, to avoid accidentally warning of sanitization just
|
||
// because characters were re-encoded.
|
||
public sanitizedSomething = false;
|
||
private buf: string[] = [];
|
||
|
||
sanitizeChildren(el: Element): string {
|
||
// This cannot use a TreeWalker, as it has to run on Angular's various DOM adapters.
|
||
// 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 !;
|
||
while (current) {
|
||
if (current.nodeType === Node.ELEMENT_NODE) {
|
||
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 (current.firstChild) {
|
||
current = current.firstChild !;
|
||
continue;
|
||
}
|
||
while (current) {
|
||
// Leaving the element. Walk up and to the right, closing tags as we go.
|
||
if (current.nodeType === Node.ELEMENT_NODE) {
|
||
this.endElement(current as Element);
|
||
}
|
||
|
||
let next = this.checkClobberedElement(current, current.nextSibling !);
|
||
|
||
if (next) {
|
||
current = next;
|
||
break;
|
||
}
|
||
|
||
current = this.checkClobberedElement(current, current.parentNode !);
|
||
}
|
||
}
|
||
return this.buf.join('');
|
||
}
|
||
|
||
private startElement(element: Element) {
|
||
const tagName = element.nodeName.toLowerCase();
|
||
if (!VALID_ELEMENTS.hasOwnProperty(tagName)) {
|
||
this.sanitizedSomething = true;
|
||
return;
|
||
}
|
||
this.buf.push('<');
|
||
this.buf.push(tagName);
|
||
const elAttrs = element.attributes;
|
||
for (let i = 0; i < elAttrs.length; i++) {
|
||
const elAttr = elAttrs.item(i);
|
||
const attrName = elAttr !.name;
|
||
const lower = attrName.toLowerCase();
|
||
if (!VALID_ATTRS.hasOwnProperty(lower)) {
|
||
this.sanitizedSomething = true;
|
||
continue;
|
||
}
|
||
let value = elAttr !.value;
|
||
// TODO(martinprobst): Special case image URIs for data:image/...
|
||
if (URI_ATTRS[lower]) value = _sanitizeUrl(value);
|
||
if (SRCSET_ATTRS[lower]) value = sanitizeSrcset(value);
|
||
this.buf.push(' ', attrName, '="', encodeEntities(value), '"');
|
||
}
|
||
this.buf.push('>');
|
||
}
|
||
|
||
private endElement(current: Element) {
|
||
const tagName = current.nodeName.toLowerCase();
|
||
if (VALID_ELEMENTS.hasOwnProperty(tagName) && !VOID_ELEMENTS.hasOwnProperty(tagName)) {
|
||
this.buf.push('</');
|
||
this.buf.push(tagName);
|
||
this.buf.push('>');
|
||
}
|
||
}
|
||
|
||
private chars(chars: string) { this.buf.push(encodeEntities(chars)); }
|
||
|
||
checkClobberedElement(node: Node, nextNode: Node): Node {
|
||
if (nextNode &&
|
||
(node.compareDocumentPosition(nextNode) &
|
||
Node.DOCUMENT_POSITION_CONTAINED_BY) === Node.DOCUMENT_POSITION_CONTAINED_BY) {
|
||
throw new Error(
|
||
`Failed to sanitize html because the element is clobbered: ${(node as Element).outerHTML}`);
|
||
}
|
||
return nextNode;
|
||
}
|
||
}
|
||
|
||
// Regular Expressions for parsing tags and attributes
|
||
const SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
|
||
// ! to ~ is the ASCII range.
|
||
const NON_ALPHANUMERIC_REGEXP = /([^\#-~ |!])/g;
|
||
|
||
/**
|
||
* Escapes all potentially dangerous characters, so that the
|
||
* resulting string can be safely inserted into attribute or
|
||
* element text.
|
||
* @param value
|
||
*/
|
||
function encodeEntities(value: string) {
|
||
return value.replace(/&/g, '&')
|
||
.replace(
|
||
SURROGATE_PAIR_REGEXP,
|
||
function(match: string) {
|
||
const hi = match.charCodeAt(0);
|
||
const low = match.charCodeAt(1);
|
||
return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
|
||
})
|
||
.replace(
|
||
NON_ALPHANUMERIC_REGEXP,
|
||
function(match: string) { return '&#' + match.charCodeAt(0) + ';'; })
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>');
|
||
}
|
||
|
||
let inertBodyHelper: InertBodyHelper;
|
||
|
||
/**
|
||
* 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 {
|
||
let inertBodyElement: HTMLElement|null = null;
|
||
try {
|
||
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
|
||
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
|
||
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
|
||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||
|
||
// mXSS protection. Repeatedly parse the document to make sure it stabilizes, so that a browser
|
||
// trying to auto-correct incorrect HTML cannot cause formerly inert HTML to become dangerous.
|
||
let mXSSAttempts = 5;
|
||
let parsedHtml = unsafeHtml;
|
||
|
||
do {
|
||
if (mXSSAttempts === 0) {
|
||
throw new Error('Failed to sanitize html because the input is unstable');
|
||
}
|
||
mXSSAttempts--;
|
||
|
||
unsafeHtml = parsedHtml;
|
||
parsedHtml = inertBodyElement !.innerHTML;
|
||
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
|
||
} while (unsafeHtml !== parsedHtml);
|
||
|
||
const sanitizer = new SanitizingHtmlSerializer();
|
||
const safeHtml = sanitizer.sanitizeChildren(
|
||
getTemplateContent(inertBodyElement !) as Element || inertBodyElement);
|
||
if (isDevMode() && sanitizer.sanitizedSomething) {
|
||
console.warn(
|
||
'WARNING: sanitizing HTML stripped some content (see http://g.co/ng/security#xss).');
|
||
}
|
||
|
||
return safeHtml;
|
||
} finally {
|
||
// In case anything goes wrong, clear out inertElement to reset the entire DOM structure.
|
||
if (inertBodyElement) {
|
||
const parent = getTemplateContent(inertBodyElement) || inertBodyElement;
|
||
while (parent.firstChild) {
|
||
parent.removeChild(parent.firstChild);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function getTemplateContent(el: Node): Node|null {
|
||
return 'content' in (el as any /** Microsoft/TypeScript#21517 */) && isTemplateElement(el) ?
|
||
el.content :
|
||
null;
|
||
}
|
||
function isTemplateElement(el: Node): el is HTMLTemplateElement {
|
||
return el.nodeType === Node.ELEMENT_NODE && el.nodeName === 'TEMPLATE';
|
||
}
|