diff --git a/goldens/public-api/platform-browser/errors.api.md b/goldens/public-api/platform-browser/errors.api.md index ead51d7ea4f..36cc6d35190 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 e2bc1c7a81f..bc9229db0b6 100644 --- a/packages/platform-browser/src/browser/meta.ts +++ b/packages/platform-browser/src/browser/meta.ts @@ -7,11 +7,14 @@ */ import {DOCUMENT, ɵDomAdapter as DomAdapter, ɵgetDOM as getDOM} from '@angular/common'; -import {Inject, Injectable} from '@angular/core'; +import {Inject, Injectable, ɵRuntimeError as RuntimeError} 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} @@ -133,6 +136,7 @@ export class Meta { */ updateTag(tag: MetaDefinition, selector?: string): HTMLMetaElement | null { if (!tag) return null; + this._validateMetaDefinition(tag); selector = selector || this._parseSelector(tag); const meta: HTMLMetaElement = this.getTag(selector)!; if (meta) { @@ -164,6 +168,7 @@ export class Meta { meta: MetaDefinition, forceCreation: boolean = false, ): HTMLMetaElement { + this._validateMetaDefinition(meta); if (!forceCreation) { const selector: string = this._parseSelector(meta); // It's allowed to have multiple elements with the same name so it's not enough to @@ -197,6 +202,19 @@ export class Meta { return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`; } + private _validateMetaDefinition(tag: MetaDefinition): void { + for (const prop of Object.keys(tag)) { + const attributeName = this._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.`, + ); + } + } + } + private _containsAttributes(tag: MetaDefinition, elem: HTMLMetaElement): boolean { return Object.keys(tag).every( (key: string) => elem.getAttribute(this._getMetaKeyMap(key)) === tag[key], @@ -204,7 +222,7 @@ export class Meta { } private _getMetaKeyMap(prop: string): string { - return META_KEYS_MAP[prop] || prop; + return Object.hasOwn(META_KEYS_MAP, prop) ? META_KEYS_MAP[prop] : prop; } } diff --git a/packages/platform-browser/src/errors.ts b/packages/platform-browser/src/errors.ts index 432b6382594..5c964975f22 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 377b5930005..d0e048056f0 100644 --- a/packages/platform-browser/test/browser/meta_spec.ts +++ b/packages/platform-browser/test/browser/meta_spec.ts @@ -89,6 +89,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 @@ -143,6 +163,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'}); @@ -155,6 +211,22 @@ describe('Meta service', () => { metaService.removeTagElement(actual); }); + it('should add attributes whose names match Object prototype keys', () => { + const meta = metaService.addTag({ + name: 'prototype-keys', + constructor: 'constructor', + toString: 'toString', + ['__proto__']: '__proto__', + })!; + + expect(meta.getAttribute('constructor')).toEqual('constructor'); + expect(meta.getAttribute('toString')).toEqual('toString'); + expect(meta.getAttribute('__proto__')).toEqual('__proto__'); + + // clean up + metaService.removeTagElement(meta); + }); + it('should escape selector values when deriving the match selector', () => { // This payload attempts to prematurely close the attribute selector // and match another attribute.