fix(compiler): move detection of unsafe properties for binding to ElementSchemaRegistry (#11378)

This commit is contained in:
Marc Laval 2016-09-28 02:10:02 +02:00 committed by Rado Kirov
parent 3a5b4882bc
commit 61129fa12d
8 changed files with 1512 additions and 1395 deletions

View File

@ -344,4 +344,26 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
getMappedPropName(propName: string): string { return _ATTR_TO_PROP[propName] || propName; }
getDefaultComponentElementName(): string { return 'ng-component'; }
validateProperty(name: string): {error: boolean, msg?: string} {
if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event property '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...` +
`\nIf '${name}' is a directive input, make sure the directive is imported by the` +
` current module.`;
return {error: true, msg: msg};
} else {
return {error: false};
}
}
validateAttribute(name: string): {error: boolean, msg?: string} {
if (name.toLowerCase().startsWith('on')) {
const msg = `Binding to event attribute '${name}' is disallowed for security reasons, ` +
`please use (${name.slice(2)})=...`;
return {error: true, msg: msg};
} else {
return {error: false};
}
}
}

View File

@ -14,4 +14,6 @@ export abstract class ElementSchemaRegistry {
abstract securityContext(tagName: string, propName: string): any;
abstract getMappedPropName(propName: string): string;
abstract getDefaultComponentElementName(): string;
abstract validateProperty(name: string): {error: boolean, msg?: string};
abstract validateAttribute(name: string): {error: boolean, msg?: string};
}

View File

@ -927,7 +927,7 @@ class TemplateParseVisitor implements html.Visitor {
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
bindingType = PropertyBindingType.Property;
this._assertNoEventBinding(boundPropertyName, sourceSpan, false);
this._validatePropertyOrAttributeName(boundPropertyName, sourceSpan, false);
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) {
let errorMsg =
`Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`;
@ -942,7 +942,7 @@ class TemplateParseVisitor implements html.Visitor {
} else {
if (parts[0] == ATTRIBUTE_PREFIX) {
boundPropertyName = parts[1];
this._assertNoEventBinding(boundPropertyName, sourceSpan, true);
this._validatePropertyOrAttributeName(boundPropertyName, sourceSpan, true);
// NB: For security purposes, use the mapped property name, not the attribute name.
const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName);
securityContext = this._schemaRegistry.securityContext(elementName, mapPropName);
@ -975,23 +975,19 @@ class TemplateParseVisitor implements html.Visitor {
boundPropertyName, bindingType, securityContext, ast, unit, sourceSpan);
}
/**
* @param propName the name of the property / attribute
* @param sourceSpan
* @param isAttr true when binding to an attribute
* @private
*/
private _assertNoEventBinding(propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean):
void {
if (propName.toLowerCase().startsWith('on')) {
let msg = `Binding to event attribute '${propName}' is disallowed for security reasons, ` +
`please use (${propName.slice(2)})=...`;
if (!isAttr) {
msg +=
`\nIf '${propName}' is a directive input, make sure the directive is imported by the` +
` current module.`;
}
this._reportError(msg, sourceSpan, ParseErrorLevel.FATAL);
private _validatePropertyOrAttributeName(
propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean): void {
const report = isAttr ? this._schemaRegistry.validateAttribute(propName) :
this._schemaRegistry.validateProperty(propName);
if (report.error) {
this._reportError(report.msg, sourceSpan, ParseErrorLevel.FATAL);
}
}

View File

@ -105,6 +105,47 @@ export function main() {
expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown');
});
it('should return an error message when asserting event properties', () => {
let report = registry.validateProperty('onClick');
expect(report.error).toBeTruthy();
expect(report.msg)
.toEqual(
`Binding to event property 'onClick' is disallowed for security reasons, please use (Click)=...
If 'onClick' is a directive input, make sure the directive is imported by the current module.`);
report = registry.validateProperty('onAnything');
expect(report.error).toBeTruthy();
expect(report.msg)
.toEqual(
`Binding to event property 'onAnything' is disallowed for security reasons, please use (Anything)=...
If 'onAnything' is a directive input, make sure the directive is imported by the current module.`);
});
it('should return an error message when asserting event attributes', () => {
let report = registry.validateAttribute('onClick');
expect(report.error).toBeTruthy();
expect(report.msg)
.toEqual(
`Binding to event attribute 'onClick' is disallowed for security reasons, please use (Click)=...`);
report = registry.validateAttribute('onAnything');
expect(report.error).toBeTruthy();
expect(report.msg)
.toEqual(
`Binding to event attribute 'onAnything' is disallowed for security reasons, please use (Anything)=...`);
});
it('should not return an error message when asserting non-event properties or attributes',
() => {
let report = registry.validateProperty('title');
expect(report.error).toBeFalsy();
expect(report.msg).not.toBeDefined();
report = registry.validateProperty('exotic-unknown');
expect(report.error).toBeFalsy();
expect(report.msg).not.toBeDefined();
});
it('should return security contexts for elements', () => {
expect(registry.securityContext('iframe', 'srcdoc')).toBe(SecurityContext.HTML);
expect(registry.securityContext('p', 'innerHTML')).toBe(SecurityContext.HTML);

View File

@ -26,7 +26,8 @@ const someModuleUrl = 'package:someModule';
const MOCK_SCHEMA_REGISTRY = [{
provide: ElementSchemaRegistry,
useValue: new MockSchemaRegistry(
{'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false}),
{'invalidProp': false}, {'mappedAttr': 'mappedProp'}, {'unknown': false, 'un-known': false},
['onEvent'], ['onEvent']),
}];
export function main() {
@ -141,7 +142,8 @@ export function main() {
});
});
describe('TemplateParser', () => {
describe(
'TemplateParser', () => {
beforeEach(() => {
TestBed.configureCompiler({providers: [TEST_COMPILER_PROVIDERS, MOCK_SCHEMA_REGISTRY]});
});
@ -276,12 +278,26 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
2. If 'unknown' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<unknown></unknown>"): TestComp@0:0`);
});
it('should throw error when binding to an unknown custom element w/o bindings', () => {
expect(() => parse('<un-known></un-known>', [])).toThrowError(`Template parse errors:
it('should throw error when binding to an unknown custom element w/o bindings',
() => {
expect(() => parse('<un-known></un-known>', []))
.toThrowError(`Template parse errors:
'un-known' is not a known element:
1. If 'un-known' is an Angular component, then verify that it is part of this module.
2. If 'un-known' is a Web Component then add "CUSTOM_ELEMENTS_SCHEMA" to the '@NgModule.schemas' of this component to suppress this message. ("[ERROR ->]<un-known></un-known>"): TestComp@0:0`);
});
it('should throw error when binding to an invalid property', () => {
expect(() => parse('<my-component [onEvent]="bar"></my-component>', []))
.toThrowError(`Template parse errors:
Binding to property 'onEvent' is disallowed for security reasons ("<my-component [ERROR ->][onEvent]="bar"></my-component>"): TestComp@0:14`);
});
it('should throw error when binding to an invalid attribute', () => {
expect(() => parse('<my-component [attr.onEvent]="bar"></my-component>', []))
.toThrowError(`Template parse errors:
Binding to attribute 'onEvent' is disallowed for security reasons ("<my-component [ERROR ->][attr.onEvent]="bar"></my-component>"): TestComp@0:14`);
});
});
it('should parse bound properties via [...] and not report them as attributes', () => {
@ -298,16 +314,20 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
]);
});
it('should parse bound properties via {{...}} and not report them as attributes', () => {
it('should parse bound properties via {{...}} and not report them as attributes',
() => {
expect(humanizeTplAst(parse('<div prop="{{v}}">', []))).toEqual([
[ElementAst, 'div'],
[BoundElementPropertyAst, PropertyBindingType.Property, 'prop', '{{ v }}', null]
[
BoundElementPropertyAst, PropertyBindingType.Property, 'prop', '{{ v }}', null
]
]);
});
it('should parse bound properties via bind-animate- and not report them as attributes',
() => {
expect(humanizeTplAst(parse('<div bind-animate-someAnimation="value2">', [], [], [])))
expect(
humanizeTplAst(parse('<div bind-animate-someAnimation="value2">', [], [], [])))
.toEqual([
[ElementAst, 'div'],
[
@ -473,8 +493,8 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
});
expect(humanizeTplAst(parse('<div a c b a b>', [dirA, dirB, dirC]))).toEqual([
[ElementAst, 'div'], [AttrAst, 'a', ''], [AttrAst, 'c', ''], [AttrAst, 'b', ''],
[AttrAst, 'a', ''], [AttrAst, 'b', ''], [DirectiveAst, dirA], [DirectiveAst, dirB],
[DirectiveAst, dirC]
[AttrAst, 'a', ''], [AttrAst, 'b', ''], [DirectiveAst, dirA],
[DirectiveAst, dirB], [DirectiveAst, dirC]
]);
});
@ -554,7 +574,8 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
inputs: ['b:a']
});
expect(humanizeTplAst(parse('<div [a]="expr"></div>', [dirA]))).toEqual([
[ElementAst, 'div'], [DirectiveAst, dirA], [BoundDirectivePropertyAst, 'b', 'expr']
[ElementAst, 'div'], [DirectiveAst, dirA],
[BoundDirectivePropertyAst, 'b', 'expr']
]);
});
@ -632,8 +653,12 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
isHost = true;
value = value.substring(5);
}
return new CompileDiDependencyMetadata(
{token: createToken(value), isOptional: isOptional, isSelf: isSelf, isHost: isHost});
return new CompileDiDependencyMetadata({
token: createToken(value),
isOptional: isOptional,
isSelf: isSelf,
isHost: isHost
});
}
function createProvider(
@ -649,7 +674,8 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
}
function createDir(
selector: string, {providers = null, viewProviders = null, deps = [], queries = []}: {
selector: string,
{providers = null, viewProviders = null, deps = [], queries = []}: {
providers?: CompileProviderMetadata[],
viewProviders?: CompileProviderMetadata[],
deps?: string[],
@ -802,7 +828,8 @@ Can't bind to 'invalidProp' since it isn't a known property of 'my-component'.
it('should mark directives and dependencies of directives as eager', () => {
var provider0 = createProvider('service0');
var provider1 = createProvider('service1');
var dirA = createDir('[dirA]', {providers: [provider0, provider1], deps: ['service0']});
var dirA =
createDir('[dirA]', {providers: [provider0, provider1], deps: ['service0']});
var elAst: ElementAst = <ElementAst>parse('<div dirA>', [dirA])[0];
expect(elAst.providers.length).toBe(3);
expect(elAst.providers[0].providers).toEqual([provider0]);
@ -1072,8 +1099,9 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
{moduleUrl: someModuleUrl, name: 'DirB', reference: {} as Type<any>})
});
expect(humanizeTplAst(parse('<div template="a b" b>', [dirA, dirB]))).toEqual([
[EmbeddedTemplateAst], [DirectiveAst, dirA], [BoundDirectivePropertyAst, 'a', 'b'],
[ElementAst, 'div'], [AttrAst, 'b', ''], [DirectiveAst, dirB]
[EmbeddedTemplateAst], [DirectiveAst, dirA],
[BoundDirectivePropertyAst, 'a', 'b'], [ElementAst, 'div'], [AttrAst, 'b', ''],
[DirectiveAst, dirB]
]);
});
@ -1148,8 +1176,9 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
describe('project text nodes', () => {
it('should project text nodes with wildcard selector', () => {
expect(humanizeContentProjection(parse('<div>hello</div>', [createComp('div', ['*'])])))
.toEqual([['div', null], ['#text(hello)', 0]]);
expect(humanizeContentProjection(parse('<div>hello</div>', [
createComp('div', ['*'])
]))).toEqual([['div', null], ['#text(hello)', 0]]);
});
});
@ -1222,9 +1251,10 @@ Reference "#a" is defined several times ("<div #a></div><div [ERROR ->]#a></div>
});
it('should project children of components with ngNonBindable', () => {
expect(humanizeContentProjection(parse('<div ngNonBindable>{{hello}}<span></span></div>', [
createComp('div', ['*'])
]))).toEqual([['div', null], ['#text({{hello}})', 0], ['span', 0]]);
expect(
humanizeContentProjection(parse(
'<div ngNonBindable>{{hello}}<span></span></div>', [createComp('div', ['*'])])))
.toEqual([['div', null], ['#text({{hello}})', 0], ['span', 0]]);
});
it('should match the element when there is an inline template', () => {
@ -1302,7 +1332,8 @@ Can't have multiple template bindings on one element. Use only one attribute nam
});
it('should report invalid property names', () => {
expect(() => parse('<div [invalidProp]></div>', [])).toThrowError(`Template parse errors:
expect(() => parse('<div [invalidProp]></div>', []))
.toThrowError(`Template parse errors:
Can't bind to 'invalidProp' since it isn't a known property of 'div'. ("<div [ERROR ->][invalidProp]></div>"): TestComp@0:5`);
});
@ -1430,7 +1461,8 @@ Property binding a not used by any directive on an embedded template. Make sure
});
it('should keep nested children of elements with ngNonBindable', () => {
expect(humanizeTplAst(parse('<div ngNonBindable><span>{{b}}</span></div>', []))).toEqual([
expect(humanizeTplAst(parse('<div ngNonBindable><span>{{b}}</span></div>', [])))
.toEqual([
[ElementAst, 'div'], [AttrAst, 'ngNonBindable', ''], [ElementAst, 'span'],
[TextAst, '{{b}}']
]);
@ -1454,10 +1486,11 @@ Property binding a not used by any directive on an embedded template. Make sure
it('should convert <ng-content> elements into regular elements inside of elements with ngNonBindable',
() => {
expect(humanizeTplAst(parse('<div ngNonBindable><ng-content></ng-content>a</div>', [])))
expect(
humanizeTplAst(parse('<div ngNonBindable><ng-content></ng-content>a</div>', [])))
.toEqual([
[ElementAst, 'div'], [AttrAst, 'ngNonBindable', ''], [ElementAst, 'ng-content'],
[TextAst, 'a']
[ElementAst, 'div'], [AttrAst, 'ngNonBindable', ''],
[ElementAst, 'ng-content'], [TextAst, 'a']
]);
});
@ -1490,8 +1523,10 @@ Property binding a not used by any directive on an embedded template. Make sure
});
it('should support variables', () => {
expect(humanizeTplAstSourceSpans(parse('<template let-a="b"></template>', []))).toEqual([
[EmbeddedTemplateAst, '<template let-a="b">'], [VariableAst, 'a', 'b', 'let-a="b"']
expect(humanizeTplAstSourceSpans(parse('<template let-a="b"></template>', [])))
.toEqual([
[EmbeddedTemplateAst, '<template let-a="b">'],
[VariableAst, 'a', 'b', 'let-a="b"']
]);
});
@ -1536,8 +1571,8 @@ Property binding a not used by any directive on an embedded template. Make sure
template: new CompileTemplateMetadata({ngContentSelectors: []})
});
expect(humanizeTplAstSourceSpans(parse('<div a>', [dirA, comp]))).toEqual([
[ElementAst, 'div', '<div a>'], [AttrAst, 'a', '', 'a'], [DirectiveAst, dirA, '<div a>'],
[DirectiveAst, comp, '<div a>']
[ElementAst, 'div', '<div a>'], [AttrAst, 'a', '', 'a'],
[DirectiveAst, dirA, '<div a>'], [DirectiveAst, comp, '<div a>']
]);
});
@ -1573,7 +1608,8 @@ Property binding a not used by any directive on an embedded template. Make sure
inputs: ['aProp']
});
expect(humanizeTplAstSourceSpans(parse('<div [aProp]="foo"></div>', [dirA]))).toEqual([
[ElementAst, 'div', '<div [aProp]="foo">'], [DirectiveAst, dirA, '<div [aProp]="foo">'],
[ElementAst, 'div', '<div [aProp]="foo">'],
[DirectiveAst, dirA, '<div [aProp]="foo">'],
[BoundDirectivePropertyAst, 'aProp', 'foo', '[aProp]="foo"']
]);
});
@ -1605,8 +1641,8 @@ The pipe 'test' could not be found ("[ERROR ->]{{a | test}}"): TestComp@0:0`);
'<template ngPluralCase="many">big</template>' +
'</ng-container>';
expect(humanizeTplAst(parse(shortForm, []))).toEqual(humanizeTplAst(parse(expandedForm, [
])));
expect(humanizeTplAst(parse(shortForm, [
]))).toEqual(humanizeTplAst(parse(expandedForm, [])));
});
it('should expand other messages', () => {
@ -1616,8 +1652,8 @@ The pipe 'test' could not be found ("[ERROR ->]{{a | test}}"): TestComp@0:0`);
'<template ngSwitchCase="other">bar</template>' +
'</ng-container>';
expect(humanizeTplAst(parse(shortForm, []))).toEqual(humanizeTplAst(parse(expandedForm, [
])));
expect(humanizeTplAst(parse(shortForm, [
]))).toEqual(humanizeTplAst(parse(expandedForm, [])));
});
it('should be possible to escape ICU messages', () => {

View File

@ -13,7 +13,8 @@ export class MockSchemaRegistry implements ElementSchemaRegistry {
constructor(
public existingProperties: {[key: string]: boolean},
public attrPropMapping: {[key: string]: string},
public existingElements: {[key: string]: boolean}) {}
public existingElements: {[key: string]: boolean}, public invalidProperties: Array<string>,
public invalidAttributes: Array<string>) {}
hasProperty(tagName: string, property: string, schemas: SchemaMetadata[]): boolean {
const value = this.existingProperties[property];
@ -32,4 +33,23 @@ export class MockSchemaRegistry implements ElementSchemaRegistry {
getMappedPropName(attrName: string): string { return this.attrPropMapping[attrName] || attrName; }
getDefaultComponentElementName(): string { return 'ng-component'; }
validateProperty(name: string): {error: boolean, msg?: string} {
if (this.invalidProperties.indexOf(name) > -1) {
return {error: true, msg: `Binding to property '${name}' is disallowed for security reasons`};
} else {
return {error: false};
}
}
validateAttribute(name: string): {error: boolean, msg?: string} {
if (this.invalidAttributes.indexOf(name) > -1) {
return {
error: true,
msg: `Binding to attribute '${name}' is disallowed for security reasons`
};
} else {
return {error: false};
}
}
}

View File

@ -19,7 +19,7 @@ export function createUrlResolverWithoutPackagePrefix(): UrlResolver {
// internal test packages.
// TODO: get rid of it or move to a separate @angular/internal_testing package
export var TEST_COMPILER_PROVIDERS: Provider[] = [
{provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {}, {})},
{provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {}, {}, [], [])},
{provide: ResourceLoader, useClass: MockResourceLoader},
{provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix}
];

View File

@ -66,7 +66,7 @@ function declareTests({useJit}: {useJit: boolean}) {
expect(() => TestBed.createComponent(SecuredComponent))
.toThrowError(
/Binding to event attribute 'onclick' is disallowed for security reasons, please use \(click\)=.../);
/Binding to event property 'onclick' is disallowed for security reasons, please use \(click\)=.../);
});
it('should disallow binding to on* unless it is consumed by a directive', () => {