Skip to content

Commit e695379

Browse files
JeanMechepkozlowski-opensource
authored andcommitted
fix(core): harden inherit definition feature against polluted prototypes
Stop inheritance traversal before built-in prototype objects and only read `ɵcmp`/`ɵdir` when they are own properties of a super type. This prevents polluted inherited properties from being treated as Angular defs during inheritance merging. Also adds regression tests covering polluted `Object.prototype.ɵdir` and `Object.prototype.ɵcmp` to ensure polluted host metadata is not inherited.
1 parent b081641 commit e695379

2 files changed

Lines changed: 75 additions & 7 deletions

File tree

packages/core/src/render3/features/inherit_definition_feature.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {RuntimeError, RuntimeErrorCode} from '../../errors';
1010
import {Type, Writable} from '../../interface/type';
1111
import {EMPTY_ARRAY, EMPTY_OBJ} from '../../util/empty';
1212
import {fillProperties} from '../../util/property';
13+
import {NG_COMP_DEF, NG_DIR_DEF} from '../fields';
1314
import {
1415
ComponentDef,
1516
ContentQueriesFunction,
@@ -45,13 +46,20 @@ export function ɵɵInheritDefinitionFeature(
4546
let shouldInheritFields = true;
4647
const inheritanceChain: WritableDef[] = [definition];
4748

48-
while (superType) {
49+
// Only accept defs declared on the current type to avoid polluted prototype members.
50+
// Don't use getComponentDef/getDirectiveDef. This logic relies on inheritance.
51+
while (superType && superType !== Function.prototype && superType !== Object.prototype) {
4952
let superDef: DirectiveDef<any> | ComponentDef<any> | undefined = undefined;
53+
const cmpDef = Object.hasOwn(superType, NG_COMP_DEF)
54+
? ((superType as any)[NG_COMP_DEF] as ComponentDef<any>)
55+
: undefined;
56+
const dirDef = Object.hasOwn(superType, NG_DIR_DEF)
57+
? ((superType as any)[NG_DIR_DEF] as DirectiveDef<any>)
58+
: undefined;
5059
if (isComponentDef(definition)) {
51-
// Don't use getComponentDef/getDirectiveDef. This logic relies on inheritance.
52-
superDef = superType.ɵcmp || superType.ɵdir;
60+
superDef = cmpDef ?? dirDef;
5361
} else {
54-
if (superType.ɵcmp) {
62+
if (cmpDef) {
5563
throw new RuntimeError(
5664
RuntimeErrorCode.INVALID_INHERITANCE,
5765
ngDevMode &&
@@ -60,8 +68,7 @@ export function ɵɵInheritDefinitionFeature(
6068
)} is attempting to extend component ${stringifyForError(superType)}`,
6169
);
6270
}
63-
// Don't use getComponentDef/getDirectiveDef. This logic relies on inheritance.
64-
superDef = superType.ɵdir;
71+
superDef = dirDef;
6572
}
6673

6774
if (superDef) {

packages/core/test/acceptance/inherit_definition_feature_spec.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
QueryList,
2525
ViewChildren,
2626
} from '../../src/core';
27-
import {getDirectiveDef} from '../../src/render3/def_getters';
27+
import {getComponentDef, getDirectiveDef} from '../../src/render3/def_getters';
2828
import {TestBed} from '../../testing';
2929

3030
describe('inheritance', () => {
@@ -68,6 +68,67 @@ describe('inheritance', () => {
6868
);
6969
});
7070

71+
it('should ignore inherited ɵdir from polluted Object.prototype', () => {
72+
const originalDir = (Object.prototype as any).ɵdir;
73+
let hadOwnDir = false;
74+
75+
try {
76+
hadOwnDir = Object.hasOwn(Object.prototype, 'ɵdir');
77+
(Object.prototype as any).ɵdir = {
78+
hostAttrs: ['data-pwned', 'yes'],
79+
};
80+
81+
class BareBase {}
82+
83+
@Directive({
84+
selector: '[childDir]',
85+
standalone: false,
86+
})
87+
class ChildDirective extends BareBase {}
88+
89+
const dirDef = getDirectiveDef(ChildDirective)!;
90+
expect(dirDef.hostAttrs).toBe(null);
91+
} finally {
92+
if (hadOwnDir) {
93+
(Object.prototype as any).ɵdir = originalDir;
94+
} else {
95+
delete (Object.prototype as any).ɵdir;
96+
}
97+
}
98+
});
99+
100+
it('should ignore inherited ɵcmp from polluted Object.prototype', () => {
101+
const originalCmp = (Object.prototype as any).ɵcmp;
102+
let hadOwnCmp = false;
103+
104+
try {
105+
hadOwnCmp = Object.hasOwn(Object.prototype, 'ɵcmp');
106+
(Object.prototype as any).ɵcmp = {
107+
hostAttrs: ['data-pwned', 'yes'],
108+
};
109+
110+
class BareBase {}
111+
112+
@Component({
113+
selector: 'child-cmp',
114+
template: `child`,
115+
standalone: false,
116+
117+
changeDetection: ChangeDetectionStrategy.Eager,
118+
})
119+
class ChildComponent extends BareBase {}
120+
121+
const cmpDef = getComponentDef(ChildComponent)!;
122+
expect(cmpDef.hostAttrs).toBe(null);
123+
} finally {
124+
if (hadOwnCmp) {
125+
(Object.prototype as any).ɵcmp = originalCmp;
126+
} else {
127+
delete (Object.prototype as any).ɵcmp;
128+
}
129+
}
130+
});
131+
71132
describe('multiple children', () => {
72133
it("should ensure that multiple child classes don't cause multiple parent execution", () => {
73134
// Assume this inheritance:

0 commit comments

Comments
 (0)