Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 6 additions & 24 deletions packages/browser-utils/src/getNativeImplementation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { debug, isNativeFunction } from '@sentry/core';
import { DEBUG_BUILD } from './debug-build';
import { getNativeImplementationFromIframe, isNativeFunction } from '@sentry/core';
import { WINDOW } from './types';

/**
Expand Down Expand Up @@ -39,31 +38,14 @@ export function getNativeImplementation<T extends keyof CacheableImplementations
return (cachedImplementations[name] = impl.bind(WINDOW) as CacheableImplementations[T]);
}

const document = WINDOW.document;
// eslint-disable-next-line deprecation/deprecation
if (document && typeof document.createElement === 'function') {
try {
const sandbox = document.createElement('iframe');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback path no longer caches, causing repeated iframe creation

Medium Severity

When getNativeImplementationFromIframe returns undefined (e.g., CSP blocks iframe creation, or iframe's contentWindow lacks the function), impl (WINDOW[name]) is returned without being stored in cachedImplementations. Previously, the inline iframe code would fall through to the final return (cachedImplementations[name] = impl.bind(WINDOW)) line because impl (the wrapped/polyfilled version from WINDOW[name]) was still truthy. Now every subsequent call to getNativeImplementation will re-attempt the iframe creation, since the cache is always empty. This particularly affects setTimeout, which is called frequently from replay-internal (debounce, click handlers, etc.).

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

sandbox.hidden = true;
document.head.appendChild(sandbox);
const contentWindow = sandbox.contentWindow;
if (contentWindow?.[name]) {
impl = contentWindow[name] as CacheableImplementations[T];
}
document.head.removeChild(sandbox);
} catch (e) {
// Could not create sandbox iframe, just use window.xxx
DEBUG_BUILD && debug.warn(`Could not create sandbox iframe for ${name} check, bailing to window.${name}: `, e);
}
}

// Sanity check: This _should_ not happen, but if it does, we just skip caching...
// This can happen e.g. in tests where fetch may not be available in the env, or similar.
if (!impl) {
const nativeImpl = getNativeImplementationFromIframe(name);
if (!nativeImpl) {
// Sanity check: this _should_ not happen, but if it does, we just skip caching...
// This can happen e.g. in tests where fetch may not be available in the env, or similar.
return impl;
}

return (cachedImplementations[name] = impl.bind(WINDOW) as CacheableImplementations[T]);
return (cachedImplementations[name] = nativeImpl.bind(WINDOW) as CacheableImplementations[T]);
}

/** Clear a cached implementation. */
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export { addClickKeypressInstrumentationHandler } from './instrument/dom';

export { addHistoryInstrumentationHandler } from './instrument/history';

export { fetch, setTimeout, clearCachedImplementation, getNativeImplementation } from './getNativeImplementation';
export { fetch, setTimeout, getNativeImplementation, clearCachedImplementation } from './getNativeImplementation';

export { addXhrInstrumentationHandler, SENTRY_XHR_DATA_KEY } from './instrument/xhr';

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,9 @@ export {
isSyntheticEvent,
isThenable,
isVueViewModel,
isNativeFunction,
} from './utils/is';
export { getNativeImplementationFromIframe } from './utils/getNativeImplementationFromIframe';
export { isBrowser } from './utils/isBrowser';
export { CONSOLE_LEVELS, consoleSandbox, debug, originalConsoleMethods } from './utils/debug-logger';
export type { SentryDebugLogger } from './utils/debug-logger';
Expand Down Expand Up @@ -259,7 +261,6 @@ export {
export { filenameIsInApp, node, nodeStackLineParser } from './utils/node-stack-trace';
export { isMatchingPattern, safeJoin, snipLine, stringMatchesSomePattern, truncate } from './utils/string';
export {
isNativeFunction,
supportsDOMError,
supportsDOMException,
supportsErrorEvent,
Expand Down
32 changes: 32 additions & 0 deletions packages/core/src/utils/getNativeImplementationFromIframe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DEBUG_BUILD } from '../debug-build';
import { GLOBAL_OBJ } from './worldwide';
import { debug } from './debug-logger';

const WINDOW = GLOBAL_OBJ as unknown as Window;

interface CacheableImplementations {
setTimeout: typeof WINDOW.setTimeout;
fetch: typeof WINDOW.fetch;
}

export function getNativeImplementationFromIframe<T extends keyof CacheableImplementations>(name: T) {
let impl = undefined;
const document = WINDOW.document;
// eslint-disable-next-line deprecation/deprecation
if (document && typeof document.createElement === 'function') {
try {
const sandbox = document.createElement('iframe');
sandbox.hidden = true;
document.head.appendChild(sandbox);
const contentWindow = sandbox.contentWindow;
if (contentWindow?.[name]) {
impl = contentWindow[name] as CacheableImplementations[T];
}
document.head.removeChild(sandbox);
} catch (e) {
// Could not create sandbox iframe, just use window.xxx
DEBUG_BUILD && debug.warn(`Could not create sandbox iframe for ${name} check, bailing to window.${name}: `, e);
}
}
return impl;
}
8 changes: 8 additions & 0 deletions packages/core/src/utils/is.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,11 @@ export function isVueViewModel(wat: unknown): wat is VueViewModel | VNode {
export function isRequest(request: unknown): request is Request {
return typeof Request !== 'undefined' && isInstanceOf(request, Request);
}

/**
* isNative checks if the given function is a native implementation
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export function isNativeFunction(func: Function): boolean {
return func && /^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString());
}
34 changes: 4 additions & 30 deletions packages/core/src/utils/supports.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DEBUG_BUILD } from '../debug-build';
import { debug } from './debug-logger';
import { getNativeImplementationFromIframe } from './getNativeImplementationFromIframe';
import { isNativeFunction } from './is';
import { GLOBAL_OBJ } from './worldwide';

const WINDOW = GLOBAL_OBJ as unknown as Window;
Expand Down Expand Up @@ -89,14 +89,6 @@ function _isFetchSupported(): boolean {
}
}

/**
* isNative checks if the given function is a native implementation
*/
// eslint-disable-next-line @typescript-eslint/ban-types
export function isNativeFunction(func: Function): boolean {
return func && /^function\s+\w+\(\)\s+\{\s+\[native code\]\s+\}$/.test(func.toString());
}

/**
* Tells whether current environment supports Fetch API natively
* {@link supportsNativeFetch}.
Expand All @@ -118,27 +110,9 @@ export function supportsNativeFetch(): boolean {
return true;
}

// window.fetch is implemented, but is polyfilled or already wrapped (e.g: by a chrome extension)
// so create a "pure" iframe to see if that has native fetch
let result = false;
const doc = WINDOW.document;
// eslint-disable-next-line deprecation/deprecation
if (doc && typeof (doc.createElement as unknown) === 'function') {
try {
const sandbox = doc.createElement('iframe');
sandbox.hidden = true;
doc.head.appendChild(sandbox);
if (sandbox.contentWindow?.fetch) {
// eslint-disable-next-line @typescript-eslint/unbound-method
result = isNativeFunction(sandbox.contentWindow.fetch);
}
doc.head.removeChild(sandbox);
} catch (err) {
DEBUG_BUILD && debug.warn('Could not create sandbox iframe for pure fetch check, bailing to window.fetch: ', err);
}
}
const nativeImpl = getNativeImplementationFromIframe('fetch');

return result;
return nativeImpl ? isNativeFunction(nativeImpl) : false;
}

/**
Expand Down
Loading