fix(platform-browser): disallow event handler attributes in Meta

Prevent arbitrary MetaDefinition properties from writing on* handlers directly to meta elements. Browser events can execute these handlers, including on meta elements rendered in the document body.

Reject handler attributes case-insensitively before matching or mutating tags.

Fixes #70126
This commit is contained in:
SkyZeroZx
2026-08-07 23:10:04 -05:00
committed by Jessica Janiuk
parent dca0a77488
commit c19a36c2fb
4 changed files with 96 additions and 3 deletions
@@ -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,
+21 -3
View File
@@ -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 `<meta>` 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;
}
}
+1
View File
@@ -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,
@@ -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.