diff --git a/injected/src/features/api-manipulation.js b/injected/src/features/api-manipulation.js index fa08e18533..89ef79f4ec 100644 --- a/injected/src/features/api-manipulation.js +++ b/injected/src/features/api-manipulation.js @@ -6,8 +6,9 @@ */ import ContentFeature from '../content-feature.js'; // eslint-disable-next-line no-redeclare -import { hasOwnProperty } from '../captured-globals.js'; +import { hasOwnProperty, getOwnPropertyDescriptor } from '../captured-globals.js'; import { processAttr } from '../utils.js'; +import { toStringGetTrap } from '../wrapper-utils.js'; /** * @internal @@ -112,8 +113,21 @@ export default class ApiManipulation extends ContentFeature { wrapApiDescriptor(api, key, change) { const getterValue = change.getterValue; if (getterValue) { + // If we are overriding an existing getter, we should preserve its `toString()` output + // to avoid exposing that the API was modified. If we are defining a new property, + // provide a generic "native code" getter string. + const origDescriptor = getOwnPropertyDescriptor(api, key); + const origGetter = origDescriptor?.get; + + const getter = () => processAttr(getterValue, undefined); + const getterProxy = new Proxy(getter, { + get: toStringGetTrap( + typeof origGetter === 'function' ? origGetter : getter, + typeof origGetter === 'function' ? undefined : `function get ${key}() { [native code] }`, + ), + }); const descriptor = { - get: () => processAttr(getterValue, undefined), + get: getterProxy, }; if ('enumerable' in change) { descriptor.enumerable = change.enumerable; diff --git a/injected/unit-test/features.js b/injected/unit-test/features.js index b46413511f..f544c4c1cb 100644 --- a/injected/unit-test/features.js +++ b/injected/unit-test/features.js @@ -128,6 +128,8 @@ describe('ApiManipulation', () => { }; apiManipulation.wrapApiDescriptor(dummyTarget, 'definedByConfig', change); expect(dummyTarget.definedByConfig).toBe('defined!'); + const desc = Object.getOwnPropertyDescriptor(dummyTarget, 'definedByConfig'); + expect(desc?.get?.toString()).toBe('function get definedByConfig() { [native code] }'); }); it('does not define a property if define is not set and property does not exist', () => { @@ -140,11 +142,14 @@ describe('ApiManipulation', () => { }); it('wraps an existing property if present', () => { + const origGetter = () => 4; Object.defineProperty(dummyTarget, 'hardwareConcurrency', { - get: () => 4, + get: origGetter, configurable: true, enumerable: true, }); + const originalDescriptor = Object.getOwnPropertyDescriptor(dummyTarget, 'hardwareConcurrency'); + const originalGetterToString = originalDescriptor?.get?.toString(); const change = { type: 'descriptor', getterValue: { type: 'number', value: 222 }, @@ -152,5 +157,7 @@ describe('ApiManipulation', () => { apiManipulation.wrapApiDescriptor(dummyTarget, 'hardwareConcurrency', change); // The getter should now return 222 expect(dummyTarget.hardwareConcurrency).toBe(222); + const updatedDescriptor = Object.getOwnPropertyDescriptor(dummyTarget, 'hardwareConcurrency'); + expect(updatedDescriptor?.get?.toString()).toBe(originalGetterToString); }); });