diff --git a/goldens/public-api/platform-browser/errors.api.md b/goldens/public-api/platform-browser/errors.api.md index 0d7a720700fd..f2f91ffacf75 100644 --- a/goldens/public-api/platform-browser/errors.api.md +++ b/goldens/public-api/platform-browser/errors.api.md @@ -13,6 +13,8 @@ export const enum RuntimeErrorCode { // (undocumented) HYDRATION_CONFLICTING_FEATURES = 5001, // (undocumented) + INVALID_EVENT_ATTRIBUTE = 5203, + // (undocumented) NO_PLUGIN_FOR_EVENT = -5101, // (undocumented) ROOT_NODE_NOT_FOUND = -5104, diff --git a/packages/platform-browser/src/browser/meta.ts b/packages/platform-browser/src/browser/meta.ts index 36e2a8bf1f7b..135a52be4eed 100644 --- a/packages/platform-browser/src/browser/meta.ts +++ b/packages/platform-browser/src/browser/meta.ts @@ -7,11 +7,14 @@ */ import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common'; -import {inject, Service} from '@angular/core'; +import {inject, ɵRuntimeError as RuntimeError, Service} from '@angular/core'; + +import {RuntimeErrorCode} from '../errors'; /** * Represents the attributes of an HTML `` element. The element itself is - * represented by the internal `HTMLMetaElement`. + * represented by the internal `HTMLMetaElement`. Event handler attributes, which start with + * `on`, are not allowed. * * @see [HTML meta tag](https://developer.mozilla.org/docs/Web/HTML/Element/meta) * @see {@link Meta} @@ -124,6 +127,7 @@ export class Meta { * @return The modified element. */ updateTag(tag: MetaDefinition, selector?: string): HTMLMetaElement | null { + validateMetaDefinition(tag); selector ??= parseSelector(tag); const meta = this.getTag(selector); if (meta) { @@ -156,6 +160,7 @@ export class Meta { meta: MetaDefinition, forceCreation: boolean = false, ): HTMLMetaElement { + validateMetaDefinition(meta); if (!forceCreation) { const selector: string = parseSelector(meta); // It's allowed to have multiple elements with the same name so it's not enough to @@ -180,6 +185,19 @@ function setMetaElementAttributes(tag: MetaDefinition, el: HTMLMetaElement) { Object.keys(tag).forEach((prop: string) => el.setAttribute(getMetaKeyMap(prop), tag[prop])); } +function validateMetaDefinition(tag: MetaDefinition): void { + for (const prop of Object.keys(tag)) { + const attributeName = getMetaKeyMap(prop); + if (attributeName.toLowerCase().startsWith('on')) { + throw new RuntimeError( + RuntimeErrorCode.INVALID_EVENT_ATTRIBUTE, + (typeof ngDevMode === 'undefined' || ngDevMode) && + `The Meta service does not allow setting event handler attribute '${attributeName}' for security reasons.`, + ); + } + } +} + function parseSelector(tag: MetaDefinition): string { const attr: string = tag.name ? 'name' : 'property'; return `${attr}=${escapeSelectorValue(String(tag[attr]))}`; diff --git a/packages/platform-browser/src/errors.ts b/packages/platform-browser/src/errors.ts index 97e17883c99e..80e5eaf6125a 100644 --- a/packages/platform-browser/src/errors.ts +++ b/packages/platform-browser/src/errors.ts @@ -26,6 +26,7 @@ export const enum RuntimeErrorCode { SANITIZATION_UNSAFE_SCRIPT = 5200, SANITIZATION_UNSAFE_RESOURCE_URL = -5201, SANITIZATION_UNEXPECTED_CTX = 5202, + INVALID_EVENT_ATTRIBUTE = 5203, // Animations related errors (5300-5400 range) ANIMATION_RENDERER_ASYNC_LOADING_FAILURE = 5300, diff --git a/packages/platform-browser/test/browser/meta_spec.ts b/packages/platform-browser/test/browser/meta_spec.ts index 314c9eeb99b1..11ee59fa2001 100644 --- a/packages/platform-browser/test/browser/meta_spec.ts +++ b/packages/platform-browser/test/browser/meta_spec.ts @@ -90,6 +90,26 @@ describe('Meta service', () => { expect(actual!.getAttribute('content')).toEqual('4321'); }); + it('should reject event handler attributes targeting a body meta tag', () => { + doc.body.appendChild(defaultMeta); + + const evil = 'alert(1)'; + + expect(metaService.getTag('property="fb:app_id"')).toBe(defaultMeta); + expect(() => + metaService.updateTag({ + property: 'fb:app_id', + style: 'content-visibility:auto', + oncontentvisibilityautostatechange: evil, + }), + ).toThrowError( + /NG05203: The Meta service does not allow setting event handler attribute 'oncontentvisibilityautostatechange'/, + ); + + expect(defaultMeta.getAttribute('style')).toBeNull(); + expect(defaultMeta.getAttribute('oncontentvisibilityautostatechange')).toBeNull(); + }); + it('should not allow a custom selector to match off target elements like the body tag', () => { // This payload attempts to break out of the `meta[name="..."]` constraint entirely // and inject a comma to target arbitrary DOM elements like the `body` tag via the @@ -144,6 +164,42 @@ describe('Meta service', () => { metaService.removeTagElement(actual); }); + it('should reject event handler attributes without adding a tag', () => { + const selector = 'name="og:title"'; + const evil = 'alert(1)'; + + expect(() => + metaService.addTag({ + name: 'og:title', + style: 'content-visibility:auto', + oncontentvisibilityautostatechange: evil, + }), + ).toThrowError( + /NG05203: The Meta service does not allow setting event handler attribute 'oncontentvisibilityautostatechange'/, + ); + + expect(metaService.getTag(selector)).toBeNull(); + }); + + it('should reject event handler attributes in addTags without adding tags', () => { + const selector = 'name="og:title"'; + const evil = 'alert(1)'; + + expect(() => + metaService.addTags([ + { + name: 'og:title', + style: 'content-visibility:auto', + oncontentvisibilityautostatechange: evil, + }, + ]), + ).toThrowError( + /NG05203: The Meta service does not allow setting event handler attribute 'oncontentvisibilityautostatechange'/, + ); + + expect(metaService.getTag(selector)).toBeNull(); + }); + it('should add httpEquiv meta tag as http-equiv', () => { metaService.addTag({httpEquiv: 'refresh', content: '3;url=http://test'});