Skip to content

Commit 567ee91

Browse files
Lms24cursoragent
andauthored
fix(core): Avoid functionToStringIntegration causing infinite recursions (#22515)
Prevent `FunctionToString` from delegating through mutable `Reflect.apply`, which can re-enter the integration when another library performs function integrity checks. This PR restores the plain wrapper while retaining the cross-origin fallback from [#2227](<#2227>) and adds a regression test for reentrant `Reflect.apply`. To be clear, the repro for this seems extremely superficial. When inspecting the supplied stack trace of the customer reporting this issue, we found some evidence for another library mocking with the functino prototype's toString(). No concrete evidence though for re-entering via `Reflect.apply()`. Refs #21965 <br>fixes [JS-3120](https://linear.app/getsentry/issue/JS-3120/investigate-possible-functionprototypetostring-regression-causing) Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 837e97c commit 567ee91

2 files changed

Lines changed: 31 additions & 15 deletions

File tree

packages/core/src/integrations/functiontostring.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Client } from '../client';
22
import { getClient } from '../currentScopes';
33
import { defineIntegration } from '../integration';
44
import type { IntegrationFn } from '../types/integration';
5+
import type { WrappedFunction } from '../types/wrappedfunction';
56
import { getOriginalFunction } from '../utils/object';
67

78
const INTEGRATION_NAME = 'FunctionToString' as const;
@@ -18,24 +19,22 @@ const _functionToStringIntegration = (() => {
1819
// intrinsics (like Function.prototype) might be immutable in some environments
1920
// e.g. Node with --frozen-intrinsics, XS (an embedded JavaScript engine) or SES (a JavaScript proposal)
2021
try {
21-
Function.prototype.toString = new Proxy(originalFunctionToString, {
22-
apply(target, thisArg, args) {
23-
const originalFunction = getOriginalFunction(thisArg);
24-
let context = thisArg;
22+
Function.prototype.toString = function (this: WrappedFunction, ...args: unknown[]): string {
23+
const originalFunction = getOriginalFunction(this);
24+
let unwrappedFunction: WrappedFunction | undefined;
2525

26-
try {
27-
if (SETUP_CLIENTS.has(getClient()!) && originalFunction) {
28-
context = originalFunction;
29-
}
30-
} catch {
31-
// Reading the Sentry carrier off `getClient()` can throw a `SecurityError` when `this` (or the global
32-
// object) is a `WindowProxy` whose browsing context was navigated cross-origin. The native
33-
// `toString` never throws here, so fall back to it to avoid turning harmless introspection into noise.
26+
try {
27+
if (SETUP_CLIENTS.has(getClient() as Client) && originalFunction !== undefined) {
28+
unwrappedFunction = originalFunction;
3429
}
30+
} catch {
31+
// Reading the Sentry carrier off `getClient()` can throw a `SecurityError` when `this` (or the global
32+
// object) is a `WindowProxy` whose browsing context was navigated cross-origin. The native
33+
// `toString` never throws here, so fall back to it to avoid turning harmless introspection into noise.
34+
}
3535

36-
return Reflect.apply(target, context, args);
37-
},
38-
});
36+
return originalFunctionToString.apply(unwrappedFunction ?? this, args);
37+
};
3938
} catch {
4039
// ignore errors here, just don't patch this
4140
}

packages/core/test/lib/integrations/functiontostring.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ describe('FunctionToString', () => {
1717

1818
afterEach(() => {
1919
vi.mocked(currentScopes.getClient).mockClear();
20+
vi.restoreAllMocks();
2021
});
2122

2223
afterAll(() => {
@@ -67,6 +68,22 @@ describe('FunctionToString', () => {
6768
expect(foo.bar.toString()).not.toBe(originalFunction);
6869
});
6970

71+
it('does not recurse when Reflect.apply performs a function toString check', () => {
72+
function inspectedFunction(): void {}
73+
74+
const fts = functionToStringIntegration();
75+
getClient()?.addIntegration(fts);
76+
const expected = inspectedFunction.toString();
77+
const originalReflectApply = Reflect.apply;
78+
79+
vi.spyOn(Reflect, 'apply').mockImplementation((target, thisArgument, argumentsList) => {
80+
target.toString();
81+
return originalReflectApply(target, thisArgument, argumentsList);
82+
});
83+
84+
expect(inspectedFunction.toString()).toBe(expected);
85+
});
86+
7087
it('falls back to native toString and does not throw when the carrier read throws', () => {
7188
const foo = {
7289
bar(wat: boolean): boolean {

0 commit comments

Comments
 (0)