Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDE
},
}]
}] });
export class HostBindingCustomSrcdocDir {
constructor() {
this.evil = 'evil';
}
}
HostBindingCustomSrcdocDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomSrcdocDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
HostBindingCustomSrcdocDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingCustomSrcdocDir, isStandalone: true, selector: "safe-srcdoc-carrier", host: { properties: { "attr.srcdoc": "evil" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomSrcdocDir, decorators: [{
type: Directive,
args: [{
selector: 'safe-srcdoc-carrier',
host: {
'[attr.srcdoc]': 'evil',
},
}]
}] });
export class HostBindingCustomSrcDir {
constructor() {
this.evil = 'evil';
}
}
HostBindingCustomSrcDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomSrcDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
HostBindingCustomSrcDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingCustomSrcDir, isStandalone: true, selector: "safe-src-carrier", host: { properties: { "attr.src": "evil" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomSrcDir, decorators: [{
type: Directive,
args: [{
selector: 'safe-src-carrier',
host: {
'[attr.src]': 'evil',
},
}]
}] });
export class HostBindingCustomDataDir {
constructor() {
this.evil = 'evil';
}
}
HostBindingCustomDataDir.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomDataDir, deps: [], target: i0.ɵɵFactoryTarget.Directive });
HostBindingCustomDataDir.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "0.0.0-PLACEHOLDER", type: HostBindingCustomDataDir, isStandalone: true, selector: "safe-data-carrier", host: { properties: { "attr.data": "evil" } }, ngImport: i0 });
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "0.0.0-PLACEHOLDER", ngImport: i0, type: HostBindingCustomDataDir, decorators: [{
type: Directive,
args: [{
selector: 'safe-data-carrier',
host: {
'[attr.data]': 'evil',
},
}]
}] });

/****************************************************************************************************
* PARTIAL FILE: sanitization.d.ts
Expand All @@ -954,6 +1002,21 @@ export declare class HostBindingSvgAnimateDir {
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingSvgAnimateDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingSvgAnimateDir, "animateMotion[hostBindingSvgAnimateDir]", never, {}, {}, never, never, true, never>;
}
export declare class HostBindingCustomSrcdocDir {
evil: string;
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingCustomSrcdocDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingCustomSrcdocDir, "safe-srcdoc-carrier", never, {}, {}, never, never, true, never>;
}
export declare class HostBindingCustomSrcDir {
evil: string;
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingCustomSrcDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingCustomSrcDir, "safe-src-carrier", never, {}, {}, never, never, true, never>;
}
export declare class HostBindingCustomDataDir {
evil: string;
static ɵfac: i0.ɵɵFactoryDeclaration<HostBindingCustomDataDir, never>;
static ɵdir: i0.ɵɵDirectiveDeclaration<HostBindingCustomDataDir, "safe-data-carrier", never, {}, {}, never, never, true, never>;
}

/****************************************************************************************************
* PARTIAL FILE: security_sensitive_constant_attributes.js
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ hostBindings: function HostBindingDir_HostBindings(rf, ctx) {
hostBindings: function HostBindingDir2_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵdomProperty("innerHTML", ctx.evil, i0.ɵɵsanitizeHtml)("href", ctx.evil, i0.ɵɵsanitizeUrl)("src", ctx.evil)("sandbox", ctx.evil);
i0.ɵɵdomProperty("innerHTML", ctx.evil, i0.ɵɵsanitizeHtml)("href", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl)("sandbox", ctx.evil, i0.ɵɵvalidateAttribute);
i0.ɵɵattribute("style", ctx.evil, i0.ɵɵsanitizeStyle);
}
}
Expand All @@ -16,4 +16,22 @@ hostBindings: function HostBindingSvgAnimateDir_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("attributeName", ctx.evil, i0.ɵɵvalidateAttribute);
}
}
}
hostBindings: function HostBindingCustomSrcdocDir_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("srcdoc", ctx.evil, i0.ɵɵsanitizeHtml);
}
}
hostBindings: function HostBindingCustomSrcDir_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("src", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl);
}
}
hostBindings: function HostBindingCustomDataDir_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("data", ctx.evil, i0.ɵɵsanitizeUrlOrResourceUrl);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,33 @@ export class HostBindingDir2 {
export class HostBindingSvgAnimateDir {
evil = 'evil';
}

@Directive({
selector: 'safe-srcdoc-carrier',
host: {
'[attr.srcdoc]': 'evil',
},
})
export class HostBindingCustomSrcdocDir {
evil = 'evil';
}

@Directive({
selector: 'safe-src-carrier',
host: {
'[attr.src]': 'evil',
},
})
export class HostBindingCustomSrcDir {
evil = 'evil';
}

@Directive({
selector: 'safe-data-carrier',
host: {
'[attr.data]': 'evil',
},
})
export class HostBindingCustomDataDir {
evil = 'evil';
}
42 changes: 37 additions & 5 deletions packages/compiler-cli/test/ngtsc/ngtsc_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8461,7 +8461,7 @@ runInEachFileSystem((os: string) => {
hostVars: 5,
hostBindings: function UnsafeAttrsDirective_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.attrAction, i0.ɵɵsanitizeUrl)("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.attrSafeTitle);
i0.ɵɵattribute("href", ctx.attrHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.attrSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.attrAction, i0.ɵɵsanitizeUrlOrResourceUrl)("innerHTML", ctx.attrInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.attrSafeTitle);
}
}
`;
Expand Down Expand Up @@ -8517,14 +8517,14 @@ runInEachFileSystem((os: string) => {
hostVars: 5,
hostBindings: function UnsafePropsDirective_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵdomProperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.propSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.propAction, i0.ɵɵsanitizeUrl)("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.propSafeTitle);
i0.ɵɵdomProperty("href", ctx.propHref, i0.ɵɵsanitizeUrlOrResourceUrl)("src", ctx.propSrc, i0.ɵɵsanitizeUrlOrResourceUrl)("action", ctx.propAction, i0.ɵɵsanitizeUrlOrResourceUrl)("innerHTML", ctx.propInnerHTML, i0.ɵɵsanitizeHtml)("title", ctx.propSafeTitle);
}
}
`;
expect(trim(jsContents)).toContain(trim(hostBindingsFn));
});

it('should not generate sanitizers for URL properties in hostBindings fn in Component', () => {
it('should generate concrete-host URL sanitizers in hostBindings fn in Component', () => {
env.write(
`test.ts`,
`
Expand Down Expand Up @@ -8552,8 +8552,40 @@ runInEachFileSystem((os: string) => {
hostVars: 6,
hostBindings: function FooCmp_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵdomProperty("src", ctx.srcProp)("href", ctx.hrefProp)("title", ctx.titleProp);
i0.ɵɵattribute("src", ctx.srcAttr)("href", ctx.hrefAttr)("title", ctx.titleAttr);
i0.ɵɵdomProperty("src", ctx.srcProp, i0.ɵɵsanitizeUrlOrResourceUrl)("href", ctx.hrefProp, i0.ɵɵsanitizeUrlOrResourceUrl)("title", ctx.titleProp);
i0.ɵɵattribute("src", ctx.srcAttr, i0.ɵɵsanitizeUrlOrResourceUrl)("href", ctx.hrefAttr, i0.ɵɵsanitizeUrlOrResourceUrl)("title", ctx.titleAttr);
}
}
`;
expect(trim(jsContents)).toContain(trim(hostBindingsFn));
});

it('should generate sanitizers for pure :not selector host bindings', () => {
env.write(
`test.ts`,
`
import {Component} from '@angular/core';

@Component({
selector: ':not(iframe)',
template: '',
host: {
'[attr.srcdoc]': 'srcdoc',
}
})
class FooCmp {
srcdoc: any;
}
`,
);

env.driveMain();
const jsContents = env.getContents('test.js');
const hostBindingsFn = `
hostVars: 1,
hostBindings: function FooCmp_HostBindings(rf, ctx) {
if (rf & 2) {
i0.ɵɵattribute("srcdoc", ctx.srcdoc, i0.ɵɵsanitizeHtml);
}
}
`;
Expand Down
2 changes: 2 additions & 0 deletions packages/compiler/src/schema/dom_element_schema_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,10 +444,12 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {

const normalizedTag = normalizeTagName(tagName);
propName = propName.toLowerCase();
const [namespace] = splitNsName(normalizedTag, false);

const securitySchema = SECURITY_SCHEMA();
const ctx =
securitySchema[normalizedTag + '|' + propName] ??
(namespace ? securitySchema[`:${namespace}:*|${propName}`] : undefined) ??
securitySchema['*|' + propName] ??
SecurityContext.NONE;

Expand Down
33 changes: 29 additions & 4 deletions packages/compiler/src/schema/dom_security_schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ export enum SecurityContext {
* Map from tagName|propertyName to SecurityContext. Properties applying to all tags use '*'.
*/
let _SECURITY_SCHEMA!: {[k: string]: SecurityContext};
const SVG_NAMESPACE = 'svg';
const MATH_ML_NAMESPACE = 'math';
export const SVG_NAMESPACE = 'svg';
export const MATH_ML_NAMESPACE = 'math';

/**
* @remarks Keep is a copy of DOM Security Schema.
Expand Down Expand Up @@ -71,6 +71,7 @@ export function SECURITY_SCHEMA(): {[k: string]: SecurityContext} {
registerContext(SecurityContext.URL, MATH_ML_NAMESPACE, [
// MathML namespace
// https://crsrc.org/c/third_party/blink/renderer/core/sanitizer/sanitizer.cc;l=753-768;drc=b3eb16372dcd3317d65e9e0265015e322494edcd;bpv=1;bpt=1
['*', ['href', 'xlink:href']],
['annotation', ['href', 'xlink:href']],
['annotation-xml', ['href', 'xlink:href']],
['maction', ['href', 'xlink:href']],
Expand Down Expand Up @@ -157,12 +158,36 @@ function registerContext(
specs: readonly [tagName: string, attributeNames: readonly string[]][],
): void {
for (const [element, attributeNames] of specs) {
let tagName =
namespace && element !== '*' && element !== 'unknown' ? `:${namespace}:${element}` : element;
let tagName = namespace && element !== 'unknown' ? `:${namespace}:${element}` : element;
tagName = tagName.toLowerCase();

for (const attr of attributeNames) {
_SECURITY_SCHEMA[`${tagName}|${attr.toLowerCase()}`] = ctx;
}
}
}

export function checkSecurityContext(
tagName: string,
propName: string,
namespace?: string | null,
): SecurityContext {
const schema = SECURITY_SCHEMA();
const normalizedTagName = tagName.toLowerCase();
const normalizedPropName = propName.toLowerCase();
const namespacedContext =
namespace && normalizedTagName !== '*' && normalizedTagName !== 'unknown'
? schema[`:${namespace}:${normalizedTagName}|${normalizedPropName}`]
: undefined;
const namespacedWildcardContext = namespace
? schema[`:${namespace}:*|${normalizedPropName}`]
: undefined;

return (
namespacedContext ??
namespacedWildcardContext ??
schema[`${normalizedTagName}|${normalizedPropName}`] ??
schema[`*|${normalizedPropName}`] ??
SecurityContext.NONE
);
}
60 changes: 49 additions & 11 deletions packages/compiler/src/template/pipeline/src/ingest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import * as t from '../../../render3/r3_ast';
import {DeferBlockDepsEmitMode, R3ComponentDeferMetadata} from '../../../render3/view/api';
import {icuFromI18nMessage} from '../../../render3/view/i18n/util';
import {DomElementSchemaRegistry} from '../../../schema/dom_element_schema_registry';
import {BindingParser} from '../../../template_parser/binding_parser';
import {BindingParser, calcPossibleSecurityContexts} from '../../../template_parser/binding_parser';
import * as ir from '../ir';

import {
Expand Down Expand Up @@ -119,19 +119,21 @@ export function ingestHostBinding(
if (property.isAnimation) {
bindingKind = ir.BindingKind.Animation;
}
const securityContexts = bindingParser
.calcPossibleSecurityContexts(
input.componentSelector,
property.name,
bindingKind === ir.BindingKind.Attribute,
)
.filter((context) => context !== SecurityContext.NONE);
const securityContexts = calcHostBindingSecurityContexts(
bindingParser,
input.componentSelector,
property.name,
bindingKind === ir.BindingKind.Attribute,
);
ingestDomProperty(job, property, bindingKind, securityContexts);
}
for (const [name, expr] of Object.entries(input.attributes) ?? []) {
const securityContexts = bindingParser
.calcPossibleSecurityContexts(input.componentSelector, name, true)
.filter((context) => context !== SecurityContext.NONE);
const securityContexts = calcHostBindingSecurityContexts(
bindingParser,
input.componentSelector,
name,
true,
);
ingestHostAttribute(job, name, expr, securityContexts);
}
for (const event of input.events ?? []) {
Expand All @@ -140,6 +142,42 @@ export function ingestHostBinding(
return job;
}

function calcHostBindingSecurityContexts(
bindingParser: BindingParser,
selector: string,
name: string,
isAttribute: boolean,
): SecurityContext[] {
const declaringSelectorContexts = bindingParser.calcPossibleSecurityContexts(
selector,
name,
isAttribute,
);
const concreteHostContexts = calcPossibleSecurityContexts(
domSchema,
null,
domSchema.getMappedPropName(name),
isAttribute,
);
const concreteHostNonNoneContexts = concreteHostContexts.filter(
(context) => context !== SecurityContext.NONE,
);
const concreteHostNonNoneCount = concreteHostNonNoneContexts.length;
const hasConcreteHostNoneContext = concreteHostNonNoneCount !== concreteHostContexts.length;

// Host bindings can run against a concrete host whose element name differs from the declaring
// selector, including dynamic root components whose TNode name is `#host`.
if (hasConcreteHostNoneContext && concreteHostNonNoneCount > 0) {
return concreteHostContexts;
}

if (concreteHostNonNoneContexts.some((context) => !declaringSelectorContexts.includes(context))) {
return concreteHostContexts;
}

return declaringSelectorContexts.filter((context) => context !== SecurityContext.NONE);
}

// TODO: We should refactor the parser to use the same types and structures for host bindings as
// with ordinary components. This would allow us to share a lot more ingestion code.
export function ingestDomProperty(
Expand Down
Loading
Loading