Skip to content

FunctionToString integration's patched Function.prototype.toString throws SecurityError when its realm's browsing context was navigated cross-origin #21965

Description

@JT-Well

Is there an existing issue for this?

  • I have checked for existing issues
  • I have reviewed the documentation
  • I am using the latest SDK release or the bug is reproducible on master

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/nuxt (the affected code is in @sentry/core, so all browser SDKs are affected)

SDK Version

10.34.0 — the affected code is unchanged on current master (packages/core/src/integrations/functiontostring.ts)

Framework Version

Nuxt 4 (not framework-specific)

Description

The functionToStringIntegration (enabled by default) patches Function.prototype.toString. The patched function calls getClient()getCurrentScope()getMainCarrier()getSentryCarrier(GLOBAL_OBJ) on every.toString() invocation, which performs a property read of GLOBAL_OBJ.__SENTRY__.

GLOBAL_OBJ is globalThis, which in a Window realm is the WindowProxy of the realm's browsing context. If that browsing context is later navigated to a cross-origin document while code from the old realm can still be invoked (e.g. a still-alive parent window holding references into a same-origin child iframe whose src changed to a cross-origin URL), the __SENTRY__ read on the WindowProxy throws:

SecurityError: Failed to read a named property '__SENTRY__' from 'Window':
Blocked a frame with origin "https://app.example.com" from accessing a cross-origin frame.

(current Chrome wording: Blocked a restricted frame with origin "…" from accessing another frame.)

Because the patch is installed on Function.prototype, the throw is triggered by any third-party script calling .toString(). In our production case the chain was: Microsoft Clarity's MutationObserver serialization → Tawk.to's core-js Function.prototype.toString wrapper → Facebook Pixel's (fbevents.js) core-js inspectSource → Sentry's patched toStringSecurityError, surfacing as onunhandledrejection noise (24 events within ~1 second from a single page transition on a page embedding a third-party game iframe with sandbox="... allow-top-navigation").

Production stack (origins redacted, resolved via source maps):

/0.8.66/clarity.js (async/await machinery, MutationObserver serialization)
/_s/v4/app/.../twk-chunk-vendors.js (ShadowRoot.toString — core-js toString wrapper)
/en_US/fbevents.js (core-js inspectSource)
@sentry/core/build/esm/integrations/functiontostring.js:24 (Function.prototype.toString)
@sentry/core/build/esm/currentScopes.js:101 (getClient)
@sentry/core/build/esm/currentScopes.js:10 (getCurrentScope)
@sentry/core/build/esm/carrier.js:18 (getMainCarrier → getSentryCarrier(GLOBAL_OBJ) → __SENTRY__ read throws)

Note: the native Function.prototype.toString never throws for these calls — the throw is introduced solely by the integration's internal carrier access, so the SDK converts harmless third-party introspection into uncatchable (for the app) SecurityError noise, which Sentry itself then reports.

Steps to Reproduce

Self-contained repro (Chrome). Serve the two files below from http://127.0.0.1:8801 and also expose the same directory on http://127.0.0.1:8802 (different port = cross-origin but same-site, any static server works):

parent.html:

<!doctype html><iframeid="f" src="/child.html"></iframe><script>constf=document.getElementById('f')letdone=falsef.onload=()=>{if(done)returndone=trueconstchildFn=f.contentWindow.exposedFn// keep a reference into the child realmsetTimeout(()=>{f.src='http://127.0.0.1:8802/parent.html'},300)// navigate iframe cross-originsetTimeout(()=>{try{childFn.toString()// resolves to the child realm's patched Function.prototype.toStringconsole.log('ok')}catch(e){console.log('THREW',e.name,e.message)}},1500)}</script>

child.html (stands in for any same-origin frame that initialized a Sentry browser SDK — inlined replica of functiontostring.ts + carrier.ts so the repro needs no bundler; using the real SDK behaves identically):

<!doctype html><script>constSDK_VERSION='10.34.0'constGLOBAL_OBJ=globalThisfunctiongetSentryCarrier(carrier){const__SENTRY__=(carrier.__SENTRY__=carrier.__SENTRY__||{})__SENTRY__.version=__SENTRY__.version||SDK_VERSIONreturn(__SENTRY__[SDK_VERSION]=__SENTRY__[SDK_VERSION]||{})}functiongetMainCarrier(){getSentryCarrier(GLOBAL_OBJ);returnGLOBAL_OBJ}functiongetClient(){getMainCarrier();returnundefined}constSETUP_CLIENTS=newWeakMap()constoriginalFunctionToString=Function.prototype.toStringFunction.prototype.toString=function(...args){constoriginalFunction=this&&this.__sentry_original__constcontext=SETUP_CLIENTS.has(getClient())&&originalFunction!==undefined ? originalFunction : thisreturnoriginalFunctionToString.apply(context,args)}window.exposedFn=functionexposedFn(){}</script>

Observed output:

THREW SecurityError Failed to read a named property '__SENTRY__' from 'Window': Blocked a restricted frame with origin "http://127.0.0.1:8801" from accessing another frame.

The same read also throws in any other "realm outlives its document's origin" situation (Android Chrome same-process cross-origin navigations of the top frame show the same signature).

Expected Result

The patched Function.prototype.toString should never throw where the native implementation would not. Suggested fix — wrap the patch body, mirroring the defensive style already used around the patch installation:

Function.prototype.toString=function(this: WrappedFunction, ...args: any[]): string{try{constoriginalFunction=getOriginalFunction(this);constcontext=SETUP_CLIENTS.has(getClient()asClient)&&originalFunction!==undefined ? originalFunction : this;returnoriginalFunctionToString.apply(context,args);}catch{// e.g. SecurityError reading __SENTRY__ off a WindowProxy whose browsing context// was navigated cross-origin — fall back to native behaviorreturnoriginalFunctionToString.apply(this,args);}};

We've verified (against @sentry/core 10.34.0) that this keeps the unwrap behavior intact for live realms and degrades to exact native semantics otherwise. Happy to open a PR if this direction is acceptable.

Actual Result

SecurityError: Failed to read a named property '__SENTRY__' from 'Window': Blocked a frame with origin "…" from accessing a cross-origin frame. thrown out of Function.prototype.toString, reported as unhandled-rejection error events.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions