mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jessica Janiuk
parent
dca0a77488
commit
c19a36c2fb
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user