diff --git a/client/public/mcp-sandbox.html b/client/public/mcp-sandbox.html
index ab384b1639..27e7bb24bc 100644
--- a/client/public/mcp-sandbox.html
+++ b/client/public/mcp-sandbox.html
@@ -31,6 +31,7 @@
let innerFrame = null;
let innerFrameBlobUrl = null;
+ let innerFrameNavigated = false;
let readyInterval = null;
const SANDBOX_PREFIX = 'ui/notifications/sandbox-';
@@ -84,13 +85,13 @@
createInnerFrame(msg.params);
return;
}
- if (innerFrame && innerFrame.contentWindow) {
+ if (innerFrame && innerFrame.contentWindow && !innerFrameNavigated) {
innerFrame.contentWindow.postMessage(msg, '*');
}
return;
}
- if (innerFrame && event.source === innerFrame.contentWindow) {
+ if (innerFrame && !innerFrameNavigated && event.source === innerFrame.contentWindow) {
if (msg.method && msg.method.startsWith(SANDBOX_PREFIX)) {
return;
}
@@ -111,6 +112,25 @@
innerFrame = document.createElement('iframe');
+ // The first load is the blob document we created. A `allow-scripts` app can navigate its own
+ // frame; any further load means it left our document, so stop proxying and tear it down
+ // rather than forward host responses (proxied MCP data) to the navigated page.
+ innerFrameNavigated = false;
+ let frameLoads = 0;
+ innerFrame.addEventListener('load', () => {
+ frameLoads += 1;
+ if (frameLoads <= 1) {
+ return;
+ }
+ innerFrameNavigated = true;
+ if (innerFrameBlobUrl) {
+ URL.revokeObjectURL(innerFrameBlobUrl);
+ innerFrameBlobUrl = null;
+ }
+ innerFrame.remove();
+ innerFrame = null;
+ });
+
// Strip allow-same-origin from the inner frame regardless of what the host requested.
// Full cross-origin isolation requires the sandbox proxy to be served from a different
// origin than the host; stripping allow-same-origin provides partial mitigation when
diff --git a/packages/api/src/mcp/UserConnectionManager.ts b/packages/api/src/mcp/UserConnectionManager.ts
index 785dfb6755..e4a8ec0173 100644
--- a/packages/api/src/mcp/UserConnectionManager.ts
+++ b/packages/api/src/mcp/UserConnectionManager.ts
@@ -453,7 +453,7 @@ export abstract class UserConnectionManager {
graphTokenResolver,
});
const registry = MCPServersRegistry.getInstance();
- const { allowedDomains, allowedAddresses, useSSRFProtection } =
+ const { allowedDomains, allowedAddresses, useSSRFProtection, appsEnabled } =
await registry.resolveAllowlists({ userId: user?.id, role: user?.role });
await this.assertResolvedRuntimeConfigAllowed({
config: runtimeConfig,
@@ -472,7 +472,7 @@ export abstract class UserConnectionManager {
useSSRFProtection,
allowedDomains,
allowedAddresses,
- enableApps: registry.getAppsEnabled(),
+ enableApps: appsEnabled,
ephemeralConnection,
};