diff --git a/packages/core/src/integrations/functiontostring.ts b/packages/core/src/integrations/functiontostring.ts index f8768037db6b..86844843664f 100644 --- a/packages/core/src/integrations/functiontostring.ts +++ b/packages/core/src/integrations/functiontostring.ts @@ -2,11 +2,8 @@ import type { Client } from '../client'; import { getClient } from '../currentScopes'; import { defineIntegration } from '../integration'; import type { IntegrationFn } from '../types/integration'; -import type { WrappedFunction } from '../types/wrappedfunction'; import { getOriginalFunction } from '../utils/object'; -let originalFunctionToString: () => void; - const INTEGRATION_NAME = 'FunctionToString' as const; const SETUP_CLIENTS = new WeakMap(); @@ -16,17 +13,29 @@ const _functionToStringIntegration = (() => { name: INTEGRATION_NAME, setupOnce() { // eslint-disable-next-line @typescript-eslint/unbound-method - originalFunctionToString = Function.prototype.toString; + const originalFunctionToString = Function.prototype.toString; // intrinsics (like Function.prototype) might be immutable in some environments // e.g. Node with --frozen-intrinsics, XS (an embedded JavaScript engine) or SES (a JavaScript proposal) try { - Function.prototype.toString = function (this: WrappedFunction, ...args: unknown[]): string { - const originalFunction = getOriginalFunction(this); - const context = - SETUP_CLIENTS.has(getClient() as Client) && originalFunction !== undefined ? originalFunction : this; - return originalFunctionToString.apply(context, args); - }; + Function.prototype.toString = new Proxy(originalFunctionToString, { + apply(target, thisArg, args) { + const originalFunction = getOriginalFunction(thisArg); + let context = thisArg; + + try { + if (SETUP_CLIENTS.has(getClient()!) && originalFunction) { + context = originalFunction; + } + } catch { + // Reading the Sentry carrier off `getClient()` can throw a `SecurityError` when `this` (or the global + // object) is a `WindowProxy` whose browsing context was navigated cross-origin. The native + // `toString` never throws here, so fall back to it to avoid turning harmless introspection into noise. + } + + return Reflect.apply(target, context, args); + }, + }); } catch { // ignore errors here, just don't patch this } diff --git a/packages/core/test/lib/integrations/functiontostring.test.ts b/packages/core/test/lib/integrations/functiontostring.test.ts index 7c40de8b06f2..1e992cbd93cb 100644 --- a/packages/core/test/lib/integrations/functiontostring.test.ts +++ b/packages/core/test/lib/integrations/functiontostring.test.ts @@ -1,16 +1,27 @@ -import { afterAll, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import * as currentScopes from '../../../src/currentScopes'; import { fill, getClient, getCurrentScope, setCurrentClient } from '../../../src'; import { functionToStringIntegration } from '../../../src/integrations/functiontostring'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; +vi.mock('../../../src/currentScopes', async importOriginal => { + const actual = await importOriginal(); + return { ...actual, getClient: vi.fn(actual.getClient) }; +}); + describe('FunctionToString', () => { beforeEach(() => { const testClient = new TestClient(getDefaultTestClientOptions({})); setCurrentClient(testClient); }); + afterEach(() => { + vi.mocked(currentScopes.getClient).mockClear(); + }); + afterAll(() => { getCurrentScope().setClient(undefined); + vi.restoreAllMocks(); }); it('it works as expected', () => { @@ -55,4 +66,24 @@ describe('FunctionToString', () => { expect(foo.bar.toString()).not.toBe(originalFunction); }); + + it('falls back to native toString and does not throw when the carrier read throws', () => { + const foo = { + bar(wat: boolean): boolean { + return wat; + }, + }; + const nativeString = foo.bar.toString(); + + const fts = functionToStringIntegration(); + getClient()?.addIntegration(fts); + + // Simulate a `SecurityError` thrown while reading the Sentry carrier off a cross-origin `WindowProxy`. + vi.mocked(currentScopes.getClient).mockImplementation(() => { + throw new Error('SecurityError: Blocked a frame from accessing a cross-origin frame.'); + }); + + expect(() => foo.bar.toString()).not.toThrow(); + expect(foo.bar.toString()).toBe(nativeString); + }); });