Skip to content

Commit 0d33e2f

Browse files
committed
Draft implementation of Trusted Types support
1 parent 24ffb0b commit 0d33e2f

25 files changed

+480
-117
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"@types/semver": "^6.0.2",
9191
"@types/shelljs": "^0.8.6",
9292
"@types/systemjs": "0.19.32",
93+
"@types/trusted-types": "^1.0.6",
9394
"@types/yaml": "^1.9.7",
9495
"@types/yargs": "^15.0.5",
9596
"@webcomponents/custom-elements": "^1.1.0",

packages/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ ts_library(
1414
deps = [
1515
"//packages/zone.js/lib:zone_d_ts",
1616
"@npm//@types/hammerjs",
17+
"@npm//@types/trusted-types",
1718
],
1819
)
1920

packages/compiler/src/render3/r3_identifiers.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,7 @@ export class Identifiers {
320320
static sanitizeUrl: o.ExternalReference = {name: 'ɵɵsanitizeUrl', moduleName: CORE};
321321
static sanitizeUrlOrResourceUrl:
322322
o.ExternalReference = {name: 'ɵɵsanitizeUrlOrResourceUrl', moduleName: CORE};
323+
static trustHtml: o.ExternalReference = {name: 'ɵɵtrustHtml', moduleName: CORE};
324+
static trustScript: o.ExternalReference = {name: 'ɵɵtrustScript', moduleName: CORE};
325+
static trustResourceUrl: o.ExternalReference = {name: 'ɵɵtrustResourceUrl', moduleName: CORE};
323326
}

packages/compiler/src/render3/view/template.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
568568

569569
const nonContentSelectAttributes =
570570
ngContent.attributes.filter(attr => attr.name.toLowerCase() !== NG_CONTENT_SELECT_ATTR);
571-
const attributes = this.getAttributeExpressions(nonContentSelectAttributes, [], []);
571+
const attributes =
572+
this.getAttributeExpressions(ngContent.name, nonContentSelectAttributes, [], []);
572573

573574
if (attributes.length > 0) {
574575
parameters.push(o.literal(projectionSlotIdx), o.literalArr(attributes));
@@ -635,7 +636,7 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
635636

636637
// add attributes for directive and projection matching purposes
637638
const attributes: o.Expression[] = this.getAttributeExpressions(
638-
outputAttrs, allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs);
639+
element.name, outputAttrs, allOtherInputs, element.outputs, stylingBuilder, [], i18nAttrs);
639640
parameters.push(this.addAttrsToConsts(attributes));
640641

641642
// local refs (ex.: <div #foo #bar="baz">)
@@ -867,8 +868,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
867868
// prepare attributes parameter (including attributes used for directive matching)
868869
const [i18nStaticAttrs, staticAttrs] = partitionArray(template.attributes, hasI18nMeta);
869870
const attrsExprs: o.Expression[] = this.getAttributeExpressions(
870-
staticAttrs, template.inputs, template.outputs, undefined /* styles */,
871-
template.templateAttrs, i18nStaticAttrs);
871+
NG_TEMPLATE_TAG_NAME, staticAttrs, template.inputs, template.outputs,
872+
undefined /* styles */, template.templateAttrs, i18nStaticAttrs);
872873
parameters.push(this.addAttrsToConsts(attrsExprs));
873874

874875
// local refs (ex.: <ng-template #foo>)
@@ -1285,8 +1286,9 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
12851286
* because those values are intended to always be generated as property instructions.
12861287
*/
12871288
private getAttributeExpressions(
1288-
renderAttributes: t.TextAttribute[], inputs: t.BoundAttribute[], outputs: t.BoundEvent[],
1289-
styles?: StylingBuilder, templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
1289+
elementName: string, renderAttributes: t.TextAttribute[], inputs: t.BoundAttribute[],
1290+
outputs: t.BoundEvent[], styles?: StylingBuilder,
1291+
templateAttrs: (t.BoundAttribute|t.TextAttribute)[] = [],
12901292
i18nAttrs: (t.BoundAttribute|t.TextAttribute)[] = []): o.Expression[] {
12911293
const alreadySeen = new Set<string>();
12921294
const attrExprs: o.Expression[] = [];
@@ -1296,7 +1298,8 @@ export class TemplateDefinitionBuilder implements t.Visitor<void>, LocalResolver
12961298
if (attr.name === NG_PROJECT_AS_ATTR_NAME) {
12971299
ngProjectAsAttr = attr;
12981300
}
1299-
attrExprs.push(...getAttributeNameLiterals(attr.name), asLiteral(attr.value));
1301+
attrExprs.push(
1302+
...getAttributeNameLiterals(attr.name), trustedConstAttribute(elementName, attr));
13001303
});
13011304

13021305
// Keep ngProjectAs next to the other name, value pairs so we can verify that we match
@@ -2128,6 +2131,33 @@ export function resolveSanitizationFn(context: core.SecurityContext, isAttribute
21282131
}
21292132
}
21302133

2134+
export function resolveTrustFn(context: core.SecurityContext) {
2135+
switch (context) {
2136+
case core.SecurityContext.HTML:
2137+
return o.importExpr(R3.trustHtml);
2138+
case core.SecurityContext.SCRIPT:
2139+
return o.importExpr(R3.trustScript);
2140+
case core.SecurityContext.RESOURCE_URL:
2141+
return o.importExpr(R3.trustResourceUrl);
2142+
default:
2143+
return null;
2144+
}
2145+
}
2146+
2147+
function trustedConstAttribute(tagName: string, attr: t.TextAttribute): o.Expression {
2148+
const value = asLiteral(attr.value);
2149+
for (const isAttribute of [true, false]) {
2150+
const trustFn =
2151+
resolveTrustFn(elementRegistry.securityContext(tagName, attr.name, isAttribute));
2152+
if (trustFn) {
2153+
return o.taggedTemplate(
2154+
trustFn,
2155+
new o.TemplateLiteral([new o.TemplateLiteralElement(attr.value, attr.valueSpan)], []));
2156+
}
2157+
}
2158+
return value;
2159+
}
2160+
21312161
function isSingleElementTemplate(children: t.Node[]): children is[t.Element] {
21322162
return children.length === 1 && children[0] instanceof t.Element;
21332163
}

packages/core/src/core_render3_private_export.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,9 @@ export {
290290
ɵɵsanitizeStyle,
291291
ɵɵsanitizeUrl,
292292
ɵɵsanitizeUrlOrResourceUrl,
293+
ɵɵtrustHtml,
294+
ɵɵtrustResourceUrl,
295+
ɵɵtrustScript,
293296
} from './sanitization/sanitization';
294297
export {
295298
noSideEffects as ɵnoSideEffects,

packages/core/src/render3/i18n/i18n_parse.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import '../../util/ng_i18n_closure_mode';
1010

1111
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../../sanitization/html_sanitizer';
1212
import {getInertBodyHelper} from '../../sanitization/inert_body';
13+
import {getTrustedTypesPolicy, trustedConstSanitizer} from '../../sanitization/trusted_types';
1314
import {_sanitizeUrl, sanitizeSrcset} from '../../sanitization/url_sanitizer';
1415
import {addAllToArray} from '../../util/array_utils';
1516
import {assertEqual} from '../../util/assert';
@@ -242,7 +243,7 @@ export function i18nAttributesFirstPass(
242243
// Set attributes for Elements only, for other types (like ElementContainer),
243244
// only set inputs below
244245
if (tNode.type === TNodeType.Element) {
245-
elementAttributeInternal(tNode, lView, attrName, value, null, null);
246+
elementAttributeInternal(tNode, lView, attrName, value, trustedConstSanitizer, null);
246247
}
247248
// Check if that attribute is a directive input
248249
const dataValue = tNode.inputs !== null && tNode.inputs[attrName];
@@ -524,7 +525,7 @@ export function parseICUBlock(pattern: string): IcuExpression {
524525
function parseIcuCase(
525526
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
526527
expandoStartIndex: number): IcuCase {
527-
const inertBodyHelper = getInertBodyHelper(getDocument());
528+
const inertBodyHelper = getInertBodyHelper(getDocument(), getTrustedTypesPolicy());
528529
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
529530
if (!inertBodyElement) {
530531
throw new Error('Unable to generate inert body element');

packages/core/src/render3/interfaces/renderer.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,15 +80,18 @@ export interface ProceduralRenderer3 {
8080
parentNode(node: RNode): RElement|null;
8181
nextSibling(node: RNode): RNode|null;
8282

83-
setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void;
83+
setAttribute(
84+
el: RElement, name: string, value: string|TrustedHTML|TrustedScript|TrustedScriptURL,
85+
namespace?: string|null): void;
8486
removeAttribute(el: RElement, name: string, namespace?: string|null): void;
8587
addClass(el: RElement, name: string): void;
8688
removeClass(el: RElement, name: string): void;
8789
setStyle(
8890
el: RElement, style: string, value: any,
8991
flags?: RendererStyleFlags2|RendererStyleFlags3): void;
9092
removeStyle(el: RElement, style: string, flags?: RendererStyleFlags2|RendererStyleFlags3): void;
91-
setProperty(el: RElement, name: string, value: any): void;
93+
setProperty(el: RElement, name: string|TrustedHTML|TrustedScript|TrustedScriptURL, value: any):
94+
void;
9295
setValue(node: RText|RComment, value: string): void;
9396

9497
// TODO(misko): Deprecate in favor of addEventListener/removeEventListener
@@ -157,9 +160,11 @@ export interface RElement extends RNode {
157160
classList: RDomTokenList;
158161
className: string;
159162
textContent: string|null;
160-
setAttribute(name: string, value: string): void;
163+
setAttribute(name: string, value: string|TrustedHTML|TrustedScript|TrustedScriptURL): void;
161164
removeAttribute(name: string): void;
162-
setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void;
165+
setAttributeNS(
166+
namespaceURI: string, qualifiedName: string,
167+
value: string|TrustedHTML|TrustedScript|TrustedScriptURL): void;
163168
addEventListener(type: string, listener: EventListener, useCapture?: boolean): void;
164169
removeEventListener(type: string, listener?: EventListener, options?: boolean): void;
165170

packages/core/src/render3/interfaces/sanitization.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@
99
/**
1010
* Function used to sanitize the value before writing it into the renderer.
1111
*/
12-
export type SanitizerFn = (value: any, tagName?: string, propName?: string) => string;
12+
export type SanitizerFn = (value: any, tagName?: string, propName?: string) =>
13+
string|TrustedHTML|TrustedScript|TrustedScriptURL;

packages/core/src/render3/jit/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,4 +166,5 @@ export const angularCoreEnv: {[name: string]: Function} =
166166
'ɵɵsanitizeScript': sanitization.ɵɵsanitizeScript,
167167
'ɵɵsanitizeUrl': sanitization.ɵɵsanitizeUrl,
168168
'ɵɵsanitizeUrlOrResourceUrl': sanitization.ɵɵsanitizeUrlOrResourceUrl,
169+
'ɵɵtrustHtml': sanitization.ɵɵtrustHtml,
169170
}))();

packages/core/src/sanitization/bypass.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ export interface SafeUrl extends SafeValue {}
5858
export interface SafeResourceUrl extends SafeValue {}
5959

6060

61-
abstract class SafeValueImpl implements SafeValue {
62-
constructor(public changingThisBreaksApplicationSecurity: string) {}
61+
abstract class SafeValueImpl<T> implements SafeValue {
62+
constructor(public changingThisBreaksApplicationSecurity: string|T) {}
6363

6464
abstract getTypeName(): string;
6565

@@ -69,35 +69,35 @@ abstract class SafeValueImpl implements SafeValue {
6969
}
7070
}
7171

72-
class SafeHtmlImpl extends SafeValueImpl implements SafeHtml {
72+
class SafeHtmlImpl extends SafeValueImpl<TrustedHTML> implements SafeHtml {
7373
getTypeName() {
7474
return BypassType.Html;
7575
}
7676
}
77-
class SafeStyleImpl extends SafeValueImpl implements SafeStyle {
77+
class SafeStyleImpl extends SafeValueImpl<string> implements SafeStyle {
7878
getTypeName() {
7979
return BypassType.Style;
8080
}
8181
}
82-
class SafeScriptImpl extends SafeValueImpl implements SafeScript {
82+
class SafeScriptImpl extends SafeValueImpl<TrustedScript> implements SafeScript {
8383
getTypeName() {
8484
return BypassType.Script;
8585
}
8686
}
87-
class SafeUrlImpl extends SafeValueImpl implements SafeUrl {
87+
class SafeUrlImpl extends SafeValueImpl<string> implements SafeUrl {
8888
getTypeName() {
8989
return BypassType.Url;
9090
}
9191
}
92-
class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl {
92+
class SafeResourceUrlImpl extends SafeValueImpl<TrustedScriptURL> implements SafeResourceUrl {
9393
getTypeName() {
9494
return BypassType.ResourceUrl;
9595
}
9696
}
9797

98-
export function unwrapSafeValue(value: SafeValue): string;
98+
export function unwrapSafeValue<T>(value: SafeValue): string|T;
9999
export function unwrapSafeValue<T>(value: T): T;
100-
export function unwrapSafeValue<T>(value: T|SafeValue): T {
100+
export function unwrapSafeValue<T>(value: any): string|T {
101101
return value instanceof SafeValueImpl ? value.changingThisBreaksApplicationSecurity as any as T :
102102
value as any as T;
103103
}
@@ -137,7 +137,7 @@ export function getSanitizationBypassType(value: any): BypassType|null {
137137
* @param trustedHtml `html` string which needs to be implicitly trusted.
138138
* @returns a `html` which has been branded to be implicitly trusted.
139139
*/
140-
export function bypassSanitizationTrustHtml(trustedHtml: string): SafeHtml {
140+
export function bypassSanitizationTrustHtml(trustedHtml: string|TrustedHTML): SafeHtml {
141141
return new SafeHtmlImpl(trustedHtml);
142142
}
143143
/**
@@ -161,7 +161,7 @@ export function bypassSanitizationTrustStyle(trustedStyle: string): SafeStyle {
161161
* @param trustedScript `script` string which needs to be implicitly trusted.
162162
* @returns a `script` which has been branded to be implicitly trusted.
163163
*/
164-
export function bypassSanitizationTrustScript(trustedScript: string): SafeScript {
164+
export function bypassSanitizationTrustScript(trustedScript: string|TrustedScript): SafeScript {
165165
return new SafeScriptImpl(trustedScript);
166166
}
167167
/**
@@ -185,6 +185,7 @@ export function bypassSanitizationTrustUrl(trustedUrl: string): SafeUrl {
185185
* @param trustedResourceUrl `url` string which needs to be implicitly trusted.
186186
* @returns a `url` which has been branded to be implicitly trusted.
187187
*/
188-
export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string): SafeResourceUrl {
188+
export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string|
189+
TrustedScriptURL): SafeResourceUrl {
189190
return new SafeResourceUrlImpl(trustedResourceUrl);
190191
}

packages/core/src/sanitization/html_sanitizer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import {isDevMode} from '../util/is_dev_mode';
1010
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
11+
import {getTrustedTypesPolicy} from './trusted_types';
1112
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
1213

1314
function tagSet(tags: string): {[k: string]: boolean} {
@@ -242,10 +243,10 @@ let inertBodyHelper: InertBodyHelper;
242243
* Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to
243244
* the DOM in a browser environment.
244245
*/
245-
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
246+
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string|TrustedHTML {
246247
let inertBodyElement: HTMLElement|null = null;
247248
try {
248-
inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
249+
inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc, getTrustedTypesPolicy());
249250
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
250251
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
251252
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
@@ -274,7 +275,7 @@ export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string
274275
'WARNING: sanitizing HTML stripped some content, see http://g.co/ng/security#xss');
275276
}
276277

277-
return safeHtml;
278+
return getTrustedTypesPolicy()?.createHTML(safeHtml) ?? safeHtml;
278279
} finally {
279280
// In case anything goes wrong, clear out inertElement to reset the entire DOM structure.
280281
if (inertBodyElement) {

packages/core/src/sanitization/inert_body.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,10 @@
1313
* Default: DOMParser strategy
1414
* Fallback: InertDocument strategy
1515
*/
16-
export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
17-
return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
16+
export function getInertBodyHelper(
17+
defaultDoc: Document, trustedTypePolicy?: TrustedTypePolicy|null): InertBodyHelper {
18+
return isDOMParserAvailable() ? new DOMParserHelper(trustedTypePolicy) :
19+
new InertDocumentHelper(defaultDoc, trustedTypePolicy);
1820
}
1921

2022
export interface InertBodyHelper {
@@ -29,15 +31,20 @@ export interface InertBodyHelper {
2931
* This is the default strategy used in browsers that support it.
3032
*/
3133
class DOMParserHelper implements InertBodyHelper {
34+
constructor(private readonly trustedTypePolicy?: TrustedTypePolicy|null) {}
35+
3236
getInertBodyElement(html: string): HTMLElement|null {
3337
// We add these extra elements to ensure that the rest of the content is parsed as expected
3438
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
3539
// `<head>` tag. Note that the `<body>` tag is closed implicitly to prevent unclosed tags
3640
// in `html` from consuming the otherwise explicit `</body>` tag.
3741
html = '<body><remove></remove>' + html;
3842
try {
39-
const body = new (window as any).DOMParser().parseFromString(html, 'text/html').body as
40-
HTMLBodyElement;
43+
const body =
44+
new (window as any)
45+
.DOMParser()
46+
.parseFromString(this.trustedTypePolicy?.createHTML(html) ?? html, 'text/html')
47+
.body as HTMLBodyElement;
4148
body.removeChild(body.firstChild!);
4249
return body;
4350
} catch {
@@ -54,7 +61,8 @@ class DOMParserHelper implements InertBodyHelper {
5461
class InertDocumentHelper implements InertBodyHelper {
5562
private inertDocument: Document;
5663

57-
constructor(private defaultDoc: Document) {
64+
constructor(
65+
private defaultDoc: Document, private readonly trustedTypePolicy?: TrustedTypePolicy|null) {
5866
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
5967

6068
if (this.inertDocument.body == null) {
@@ -71,7 +79,8 @@ class InertDocumentHelper implements InertBodyHelper {
7179
// Prefer using <template> element if supported.
7280
const templateEl = this.inertDocument.createElement('template');
7381
if ('content' in templateEl) {
74-
templateEl.innerHTML = html;
82+
templateEl.innerHTML =
83+
(this.trustedTypePolicy?.createHTML(html) ?? html) as unknown as string;
7584
return templateEl;
7685
}
7786

@@ -83,7 +92,7 @@ class InertDocumentHelper implements InertBodyHelper {
8392
// down the line. This has been worked around by creating a new inert `body` and using it as
8493
// the root node in which we insert the HTML.
8594
const inertBody = this.inertDocument.createElement('body');
86-
inertBody.innerHTML = html;
95+
inertBody.innerHTML = (this.trustedTypePolicy?.createHTML(html) ?? html) as unknown as string;
8796

8897
// Support: IE 9-11 only
8998
// strip custom-namespaced attributes on IE<=11
@@ -129,7 +138,9 @@ class InertDocumentHelper implements InertBodyHelper {
129138
*/
130139
export function isDOMParserAvailable() {
131140
try {
132-
return !!new (window as any).DOMParser().parseFromString('', 'text/html');
141+
return !!new (window as any)
142+
.DOMParser()
143+
.parseFromString(window.trustedTypes?.emptyHTML ?? '', 'text/html');
133144
} catch {
134145
return false;
135146
}

0 commit comments

Comments
 (0)