Skip to content

Commit

Permalink
feat(ivy): add canonical spec for sanitization
Browse files Browse the repository at this point in the history
  • Loading branch information
mhevery committed Mar 5, 2018
1 parent 4050cf7 commit 3e60aa3
Show file tree
Hide file tree
Showing 16 changed files with 591 additions and 101 deletions.
6 changes: 3 additions & 3 deletions packages/core/src/core_private_export.ts
Expand Up @@ -18,9 +18,9 @@ export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} fr
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';
export {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn} from './reflection/types';
export {DirectRenderer as ɵDirectRenderer, RenderDebugInfo as ɵRenderDebugInfo} from './render/api';
export {sanitizeHtml as ɵsanitizeHtml} from './sanitization/html_sanitizer';
export {sanitizeStyle as ɵsanitizeStyle} from './sanitization/style_sanitizer';
export {sanitizeUrl as ɵsanitizeUrl} from './sanitization/url_sanitizer';
export {_sanitizeHtml as ɵ_sanitizeHtml} from './sanitization/html_sanitizer';
export {_sanitizeStyle as ɵ_sanitizeStyle} from './sanitization/style_sanitizer';
export {_sanitizeUrl as ɵ_sanitizeUrl} from './sanitization/url_sanitizer';
export {global as ɵglobal, looseIdentical as ɵlooseIdentical, stringify as ɵstringify} from './util';
export {makeDecorator as ɵmakeDecorator} from './util/decorators';
export {isObservable as ɵisObservable, isPromise as ɵisPromise} from './util/lang';
Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/core_render3_private_export.ts
Expand Up @@ -71,8 +71,15 @@ export {
ld as ɵld,
Pp as ɵPp,
} from './render3/index';
export {
bypassSanitizationTrustHtml as ɵbypassSanitizationTrustHtml,
bypassSanitizationTrustStyle as ɵbypassSanitizationTrustStyle,
bypassSanitizationTrustScript as ɵbypassSanitizationTrustScript,
bypassSanitizationTrustUrl as ɵbypassSanitizationTrustUrl,
bypassSanitizationTrustResourceUrl as ɵbypassSanitizationTrustResourceUrl,
sanitizeHtml as ɵsanitizeHtml,
sanitizeStyle as ɵsanitizeStyle,
sanitizeUrl as ɵsanitizeUrl,
sanitizeResourceUrl as ɵsanitizeResourceUrl,
} from './sanitization/sanitization';
// clang-format on

export {htmlSanitizer as ɵhtmlSanitizer} from './sanitization/html_sanitizer';
export {styleSanitizer as ɵstyleSanitizer} from './sanitization/style_sanitizer';
export {urlSanitizer as ɵurlSanitizer, resourceUrlSanitizer as ɵresourceUrlSanitizer} from './sanitization/url_sanitizer';
37 changes: 27 additions & 10 deletions packages/core/src/render3/instructions.ts
Expand Up @@ -38,6 +38,12 @@ export const NG_HOST_SYMBOL = '__ngHostLNode__';
*/
const _CLEAN_PROMISE = Promise.resolve(null);

/**
* Function used to stringify the value before writing it into the renderer.
* Also used for sanitization.
*/
export type Stringifier = (value: any) => string;


/**
* This property gets set before entering a template.
Expand Down Expand Up @@ -685,20 +691,23 @@ export function elementEnd() {
* Updates the value of removes an attribute on an Element.
*
* @param number index The index of the element in the data array
* @param string name The name of the attribute.
* @param any value The attribute is removed when value is `null` or `undefined`.
* @param name name The name of the attribute.
* @param value value The attribute is removed when value is `null` or `undefined`.
* Otherwise the attribute value is set to the stringified value.
* @param stringifier An optional function used to transform the value typically used for
* sanitization.
*/
export function elementAttribute(index: number, name: string, value: any): void {
export function elementAttribute(
index: number, name: string, value: any, stringifier?: Stringifier): void {
if (value !== NO_CHANGE) {
const element: LElementNode = data[index];
if (value == null) {
isProceduralRenderer(renderer) ? renderer.removeAttribute(element.native, name) :
element.native.removeAttribute(name);
} else {
isProceduralRenderer(renderer) ?
renderer.setAttribute(element.native, name, stringify(value)) :
element.native.setAttribute(name, stringify(value));
const strValue = stringifier == null ? stringify(value) : stringifier(value);
isProceduralRenderer(renderer) ? renderer.setAttribute(element.native, name, strValue) :
element.native.setAttribute(name, strValue);
}
}
}
Expand All @@ -714,9 +723,12 @@ export function elementAttribute(index: number, name: string, value: any): void
* @param propName Name of property. Because it is going to DOM, this is not subject to
* renaming as part of minification.
* @param value New value to write.
* @param stringifier An optional function used to transform the value typically used for
* sanitization.
*/

export function elementProperty<T>(index: number, propName: string, value: T | NO_CHANGE): void {
export function elementProperty<T>(
index: number, propName: string, value: T | NO_CHANGE, stringifier?: Stringifier): void {
if (value === NO_CHANGE) return;
const node = data[index] as LElementNode;
const tNode = node.tNode !;
Expand All @@ -733,6 +745,7 @@ export function elementProperty<T>(index: number, propName: string, value: T | N
setInputsForProperty(dataValue, value);
markDirtyIfOnPush(node);
} else {
value = (stringifier != null ? stringifier(value) : stringify(value)) as any;
const native = node.native;
isProceduralRenderer(renderer) ? renderer.setProperty(native, propName, value) :
(native.setProperty ? native.setProperty(propName, value) :
Expand Down Expand Up @@ -838,18 +851,22 @@ export function elementClass<T>(index: number, className: string, value: T | NO_
* @param styleName Name of property. Because it is going to DOM this is not subject to
* renaming as part of minification.
* @param value New value to write (null to remove).
* @param suffix Suffix to add to style's value (optional).
* @param suffix Optional suffix. Used with scalar values to add unit such as `px`.
* @param stringifier An optional function used to transform the value typically used for
* sanitization.
*/
export function elementStyle<T>(
index: number, styleName: string, value: T | NO_CHANGE, suffix?: string): void {
index: number, styleName: string, value: T | NO_CHANGE, suffix?: string | null,
stringifier?: Stringifier): void {
if (value !== NO_CHANGE) {
const lElement = data[index] as LElementNode;
if (value == null) {
isProceduralRenderer(renderer) ?
renderer.removeStyle(lElement.native, styleName, RendererStyleFlags3.DashCase) :
lElement.native.style.removeProperty(styleName);
} else {
const strValue = suffix ? stringify(value) + suffix : stringify(value);
let strValue = stringifier == null ? stringify(value) : stringifier(value);
if (suffix) strValue = strValue + (stringifier == null ? suffix : stringifier(suffix));
isProceduralRenderer(renderer) ?
renderer.setStyle(lElement.native, styleName, strValue, RendererStyleFlags3.DashCase) :
lElement.native.style.setProperty(styleName, strValue);
Expand Down
32 changes: 12 additions & 20 deletions packages/core/src/sanitization/html_sanitizer.ts
Expand Up @@ -6,10 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import {isDevMode} from '@angular/core';

import {isDevMode} from '../application_ref';
import {InertBodyHelper} from './inert_body';
import {sanitizeSrcset, sanitizeUrl} from './url_sanitizer';
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';

function tagSet(tags: string): {[k: string]: boolean} {
const res: {[k: string]: boolean} = {};
Expand Down Expand Up @@ -143,21 +142,17 @@ class SanitizingHtmlSerializer {
for (let i = 0; i < elAttrs.length; i++) {
const elAttr = elAttrs.item(i);
const attrName = elAttr.name;
let value = elAttr.value;
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 (URI_ATTRS[lower]) value = _sanitizeUrl(value);
if (SRCSET_ATTRS[lower]) value = sanitizeSrcset(value);
this.buf.push(' ');
this.buf.push(attrName);
this.buf.push('="');
this.buf.push(encodeEntities(value));
this.buf.push('"');
};
this.buf.push(' ', attrName, '="', encodeEntities(value), '"');
}
this.buf.push('>');
}

Expand All @@ -173,7 +168,9 @@ class SanitizingHtmlSerializer {
private chars(chars: string) { this.buf.push(encodeEntities(chars)); }

checkClobberedElement(node: Node, nextNode: Node): Node {
if (nextNode && node.contains(nextNode)) {
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}`);
}
Expand Down Expand Up @@ -214,7 +211,7 @@ 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 {
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
let inertBodyElement: HTMLElement|null = null;
try {
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
Expand Down Expand Up @@ -259,13 +256,8 @@ export function sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
}

function getTemplateContent(el: Node): Node|null {
return 'content' in el && isTemplateElement(el) ? (<any>el).content : null;
return 'content' in el && isTemplateElement(el) ? el.content : null;
}
function isTemplateElement(el: Node): boolean {
function isTemplateElement(el: Node): el is HTMLTemplateElement {
return el.nodeType === Node.ELEMENT_NODE && el.nodeName === 'TEMPLATE';
}

export function htmlSanitizer(text: string): string {
// TODO: implement;
return text;
}
4 changes: 2 additions & 2 deletions packages/core/src/sanitization/inert_body.ts
Expand Up @@ -143,12 +143,12 @@ export class InertBodyHelper {
*/
private stripCustomNsAttrs(el: Element) {
const elAttrs = el.attributes;
for (let i = 0; i < elAttrs.length; i++) {
// loop backwards so that we can support removals.
for (let i = elAttrs.length - 1; 0 < i; i++) {
const attrib = elAttrs.item(i);
const attrName = attrib.name;
if (attrName === 'xmlns:ns1' || attrName.indexOf('ns1:') === 0) {
el.removeAttribute(attrName);
i--; // compensate for the removal of attribute.
}
}
let childNode = el.firstChild;
Expand Down

0 comments on commit 3e60aa3

Please sign in to comment.