fix(compiler): move detection of unsafe properties for binding to ElementSchemaRegistry (#11378)
This commit is contained in:
parent
3a5b4882bc
commit
61129fa12d
|
@ -344,4 +344,26 @@ export class DomElementSchemaRegistry extends ElementSchemaRegistry {
|
||||||
getMappedPropName(propName: string): string { return _ATTR_TO_PROP[propName] || propName; }
|
getMappedPropName(propName: string): string { return _ATTR_TO_PROP[propName] || propName; }
|
||||||
|
|
||||||
getDefaultComponentElementName(): string { return 'ng-component'; }
|
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};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,4 +14,6 @@ export abstract class ElementSchemaRegistry {
|
||||||
abstract securityContext(tagName: string, propName: string): any;
|
abstract securityContext(tagName: string, propName: string): any;
|
||||||
abstract getMappedPropName(propName: string): string;
|
abstract getMappedPropName(propName: string): string;
|
||||||
abstract getDefaultComponentElementName(): string;
|
abstract getDefaultComponentElementName(): string;
|
||||||
|
abstract validateProperty(name: string): {error: boolean, msg?: string};
|
||||||
|
abstract validateAttribute(name: string): {error: boolean, msg?: string};
|
||||||
}
|
}
|
||||||
|
|
|
@ -927,7 +927,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
|
boundPropertyName = this._schemaRegistry.getMappedPropName(partValue);
|
||||||
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
|
securityContext = this._schemaRegistry.securityContext(elementName, boundPropertyName);
|
||||||
bindingType = PropertyBindingType.Property;
|
bindingType = PropertyBindingType.Property;
|
||||||
this._assertNoEventBinding(boundPropertyName, sourceSpan, false);
|
this._validatePropertyOrAttributeName(boundPropertyName, sourceSpan, false);
|
||||||
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) {
|
if (!this._schemaRegistry.hasProperty(elementName, boundPropertyName, this._schemas)) {
|
||||||
let errorMsg =
|
let errorMsg =
|
||||||
`Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`;
|
`Can't bind to '${boundPropertyName}' since it isn't a known property of '${elementName}'.`;
|
||||||
|
@ -942,7 +942,7 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
} else {
|
} else {
|
||||||
if (parts[0] == ATTRIBUTE_PREFIX) {
|
if (parts[0] == ATTRIBUTE_PREFIX) {
|
||||||
boundPropertyName = parts[1];
|
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.
|
// NB: For security purposes, use the mapped property name, not the attribute name.
|
||||||
const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName);
|
const mapPropName = this._schemaRegistry.getMappedPropName(boundPropertyName);
|
||||||
securityContext = this._schemaRegistry.securityContext(elementName, mapPropName);
|
securityContext = this._schemaRegistry.securityContext(elementName, mapPropName);
|
||||||
|
@ -975,23 +975,19 @@ class TemplateParseVisitor implements html.Visitor {
|
||||||
boundPropertyName, bindingType, securityContext, ast, unit, sourceSpan);
|
boundPropertyName, bindingType, securityContext, ast, unit, sourceSpan);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param propName the name of the property / attribute
|
* @param propName the name of the property / attribute
|
||||||
* @param sourceSpan
|
* @param sourceSpan
|
||||||
* @param isAttr true when binding to an attribute
|
* @param isAttr true when binding to an attribute
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private _assertNoEventBinding(propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean):
|
private _validatePropertyOrAttributeName(
|
||||||
void {
|
propName: string, sourceSpan: ParseSourceSpan, isAttr: boolean): void {
|
||||||
if (propName.toLowerCase().startsWith('on')) {
|
const report = isAttr ? this._schemaRegistry.validateAttribute(propName) :
|
||||||
let msg = `Binding to event attribute '${propName}' is disallowed for security reasons, ` +
|
this._schemaRegistry.validateProperty(propName);
|
||||||
`please use (${propName.slice(2)})=...`;
|
if (report.error) {
|
||||||
if (!isAttr) {
|
this._reportError(report.msg, sourceSpan, ParseErrorLevel.FATAL);
|
||||||
msg +=
|
|
||||||
`\nIf '${propName}' is a directive input, make sure the directive is imported by the` +
|
|
||||||
` current module.`;
|
|
||||||
}
|
|
||||||
this._reportError(msg, sourceSpan, ParseErrorLevel.FATAL);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -105,6 +105,47 @@ export function main() {
|
||||||
expect(registry.getMappedPropName('exotic-unknown')).toEqual('exotic-unknown');
|
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', () => {
|
it('should return security contexts for elements', () => {
|
||||||
expect(registry.securityContext('iframe', 'srcdoc')).toBe(SecurityContext.HTML);
|
expect(registry.securityContext('iframe', 'srcdoc')).toBe(SecurityContext.HTML);
|
||||||
expect(registry.securityContext('p', 'innerHTML')).toBe(SecurityContext.HTML);
|
expect(registry.securityContext('p', 'innerHTML')).toBe(SecurityContext.HTML);
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -13,7 +13,8 @@ export class MockSchemaRegistry implements ElementSchemaRegistry {
|
||||||
constructor(
|
constructor(
|
||||||
public existingProperties: {[key: string]: boolean},
|
public existingProperties: {[key: string]: boolean},
|
||||||
public attrPropMapping: {[key: string]: string},
|
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 {
|
hasProperty(tagName: string, property: string, schemas: SchemaMetadata[]): boolean {
|
||||||
const value = this.existingProperties[property];
|
const value = this.existingProperties[property];
|
||||||
|
@ -32,4 +33,23 @@ export class MockSchemaRegistry implements ElementSchemaRegistry {
|
||||||
getMappedPropName(attrName: string): string { return this.attrPropMapping[attrName] || attrName; }
|
getMappedPropName(attrName: string): string { return this.attrPropMapping[attrName] || attrName; }
|
||||||
|
|
||||||
getDefaultComponentElementName(): string { return 'ng-component'; }
|
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};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export function createUrlResolverWithoutPackagePrefix(): UrlResolver {
|
||||||
// internal test packages.
|
// internal test packages.
|
||||||
// TODO: get rid of it or move to a separate @angular/internal_testing package
|
// TODO: get rid of it or move to a separate @angular/internal_testing package
|
||||||
export var TEST_COMPILER_PROVIDERS: Provider[] = [
|
export var TEST_COMPILER_PROVIDERS: Provider[] = [
|
||||||
{provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {}, {})},
|
{provide: ElementSchemaRegistry, useValue: new MockSchemaRegistry({}, {}, {}, [], [])},
|
||||||
{provide: ResourceLoader, useClass: MockResourceLoader},
|
{provide: ResourceLoader, useClass: MockResourceLoader},
|
||||||
{provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix}
|
{provide: UrlResolver, useFactory: createUrlResolverWithoutPackagePrefix}
|
||||||
];
|
];
|
||||||
|
|
|
@ -66,7 +66,7 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||||
|
|
||||||
expect(() => TestBed.createComponent(SecuredComponent))
|
expect(() => TestBed.createComponent(SecuredComponent))
|
||||||
.toThrowError(
|
.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', () => {
|
it('should disallow binding to on* unless it is consumed by a directive', () => {
|
||||||
|
|
Loading…
Reference in New Issue