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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user