From 018bb28b80b7da3691a9522bdbaa09c7ab93b59c Mon Sep 17 00:00:00 2001 From: arshiya tabasum Date: Tue, 11 Aug 2026 18:25:30 +0530 Subject: [PATCH] fix(animations): detect object trigger values with Object.hasOwn StateValue and AnimationTransitionNamespace.trigger detect the {value, params} object form of a trigger binding by calling hasOwnProperty on the bound value. When that value is an object from untrusted data (for example a parsed JSON payload) carrying an own hasOwnProperty key, the shadowed property is called as a method and throws, breaking the animation flush. Use Object.hasOwn for the check so a shadowing key no longer matters. --- .../src/render/transition_animation_engine.ts | 4 ++-- .../transition_animation_engine_spec.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/animations/browser/src/render/transition_animation_engine.ts b/packages/animations/browser/src/render/transition_animation_engine.ts index 3fb49af3c05..976ea967a9e 100644 --- a/packages/animations/browser/src/render/transition_animation_engine.ts +++ b/packages/animations/browser/src/render/transition_animation_engine.ts @@ -112,7 +112,7 @@ class StateValue { input: any, public namespaceId: string = '', ) { - const isObj = input && input.hasOwnProperty('value'); + const isObj = input && Object.hasOwn(input, 'value'); const value = isObj ? input['value'] : input; this.value = normalizeTriggerValue(value); if (isObj) { @@ -246,7 +246,7 @@ class AnimationTransitionNamespace { let fromState = triggersWithStates.get(triggerName); const toState = new StateValue(value, this.id); - const isObj = value && value.hasOwnProperty('value'); + const isObj = value && Object.hasOwn(value, 'value'); if (!isObj && fromState) { toState.absorbOptions(fromState.options); } diff --git a/packages/animations/browser/test/render/transition_animation_engine_spec.ts b/packages/animations/browser/test/render/transition_animation_engine_spec.ts index 6a63b6bf1a5..7ace8eefe5f 100644 --- a/packages/animations/browser/test/render/transition_animation_engine_spec.ts +++ b/packages/animations/browser/test/render/transition_animation_engine_spec.ts @@ -118,6 +118,25 @@ const DEFAULT_NAMESPACE_ID = 'id'; expect(engine.players.length).toEqual(1); }); + it('should read the value from a trigger object whose own key shadows hasOwnProperty', () => { + const engine = makeEngine(); + + const trig = trigger('myTrigger', [ + transition('* => *', [style({height: '0px'}), animate(1000, style({height: '100px'}))]), + ]); + + registerTrigger(element, engine, trig); + + // A bound trigger value in the `{value, params}` object form can come from + // untrusted data (e.g. a parsed JSON payload) and carry an own `hasOwnProperty` + // key that shadows the method. Detecting the object form must not depend on it. + const value = JSON.parse('{"value": "matched", "hasOwnProperty": "x"}'); + expect(() => setProperty(element, engine, 'myTrigger', value)).not.toThrow(); + + engine.flush(); + expect(engine.players.length).toEqual(1); + }); + it('should throw an error if an animation property without a matching trigger is changed', () => { const engine = makeEngine(); expect(() => {