fix: allow workbench origin for remote resources (#336549)

This commit is contained in:
Robo
2026-09-17 12:25:49 +00:00
committed by GitHub
parent 9dadd5ad1d
commit 07b2aef653
8 changed files with 178 additions and 33 deletions
+3 -17
View File
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { app, BrowserWindow, desktopCapturer, Details, globalShortcut, GPUFeatureStatus, net, powerMonitor, protocol, screen as electronScreen, session, Session, systemPreferences, WebFrameMain } from 'electron';
import { app, BrowserWindow, desktopCapturer, Details, globalShortcut, GPUFeatureStatus, powerMonitor, protocol, screen as electronScreen, session, Session, systemPreferences, WebFrameMain } from 'electron';
import { addUNCHostToAllowlist, disableUNCAccessRestrictions } from '../../base/node/unc.js';
import { validatedIpcMain } from '../../base/parts/ipc/electron-main/ipcMain.js';
import { hostname, release } from 'os';
@@ -151,6 +151,7 @@ import { AgentNetworkFilterService, IAgentNetworkFilterService } from '../../pla
import { ITerminalSandboxService, NullTerminalSandboxService } from '../../platform/sandbox/common/terminalSandboxService.js';
import ErrorTelemetry from '../../platform/telemetry/electron-main/errorTelemetry.js';
import { IProtocolMainService } from '../../platform/protocol/electron-main/protocol.js';
import { createRemoteResourceRequestHandler } from '../../platform/protocol/electron-main/remoteResourceProtocol.js';
type OSProxyConfigEvent = {
readonly success: boolean;
@@ -1634,22 +1635,7 @@ export class CodeApplication extends Disposable {
this.installMutex();
// Remote Authorities
protocol.handle(Schemas.vscodeRemoteResource, async request => {
try {
return await net.fetch(
request.url.replace(/^vscode-remote-resource:/, 'http:'),
{
method: request.method,
headers: request.headers,
body: request.body,
bypassCustomProtocolHandlers: true
}
);
} catch (error) {
this.logService.warn('error loading remote resource', error);
return Response.error();
}
});
protocol.handle(Schemas.vscodeRemoteResource, createRemoteResourceRequestHandler(this.logService));
this._register(toDisposable(() => protocol.unhandle(Schemas.vscodeRemoteResource)));
// Start to fetch shell environment (if needed) after window has opened
@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { net } from 'electron';
import { Schemas } from '../../../base/common/network.js';
import { ILogService } from '../../log/common/log.js';
type RemoteResourceRequestHandler = (request: GlobalRequest) => Promise<GlobalResponse>;
export function createRemoteResourceRequestHandler(logService: ILogService): RemoteResourceRequestHandler {
return async request => {
try {
return await net.fetch(
request.url.replace(`${Schemas.vscodeRemoteResource}:`, `${Schemas.http}:`),
{
method: request.method,
headers: request.headers,
body: request.body,
bypassCustomProtocolHandlers: true
}
);
} catch (error) {
logService.warn('error loading remote resource', error);
return Response.error();
}
};
}
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { ipcRenderer } from 'electron';
import { Schemas } from '../../../../base/common/network.js';
import { URI } from '../../../../base/common/uri.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
@@ -83,4 +84,17 @@ suite('ProtocolMainService', () => {
requestCount: 1,
});
});
test('loads vscode-remote-resource with workbench CORS headers', async function () {
this.timeout(15_000);
const response = await ipcRenderer.invoke('vscode:test-remote-resource');
assert.deepStrictEqual(response, {
loaded: true,
requestHeaders: {
origin: 'vscode-file://vscode-app',
secFetchMode: 'cors',
},
});
});
});
@@ -37,6 +37,7 @@ import { ManagementConnection } from './remoteExtensionManagement.js';
import { determineServerConnectionToken, requestHasValidConnectionToken as httpRequestHasValidConnectionToken, ServerConnectionToken, ServerConnectionTokenParseError, ServerConnectionTokenType } from './serverConnectionToken.js';
import { IServerEnvironmentService, ServerParsedArgs } from './serverEnvironmentService.js';
import { IServerLifetimeService } from './serverLifetimeService.js';
import { getRemoteResourceResponseHeaders } from './remoteResourceResponse.js';
import { setupServerServices, SocketServer } from './serverServices.js';
import { CacheControl, serveError, serveFile, WebClientServer } from './webClientServer.js';
const require = createRequire(import.meta.url);
@@ -51,13 +52,6 @@ function parseRequestUrl(requestUrl: string): URL | undefined {
}
}
function getRemoteResourceResponseHeaders(): Record<string, string> {
return {
'Content-Security-Policy': `default-src 'none'; sandbox`,
'X-Content-Type-Options': 'nosniff',
};
}
declare namespace vsda {
// the signer is a native module that for historical reasons uses a lower case class name
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -181,7 +175,8 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {
return serveError(req, res, 400, `Bad request.`);
}
const responseHeaders = getRemoteResourceResponseHeaders();
const requestOrigin = req.headers['origin'];
const responseHeaders = getRemoteResourceResponseHeaders(requestOrigin, origin => this._webEndpointOriginChecker.matches(origin));
if (this._environmentService.isBuilt) {
if (isEqualOrParent(filePath, this._environmentService.builtinExtensionsPath, !platform.isLinux)
|| isEqualOrParent(filePath, this._environmentService.extensionsPath, !platform.isLinux)
@@ -190,12 +185,6 @@ class RemoteExtensionHostAgentServer extends Disposable implements IServerAPI {
}
}
// Allow cross origin requests from the web worker extension host
responseHeaders['Vary'] = 'Origin';
const requestOrigin = req.headers['origin'];
if (requestOrigin && this._webEndpointOriginChecker.matches(requestOrigin)) {
responseHeaders['Access-Control-Allow-Origin'] = requestOrigin;
}
return serveFile(filePath, CacheControl.ETAG, this._logService, req, res, responseHeaders);
}
@@ -0,0 +1,22 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Schemas, VSCODE_AUTHORITY } from '../../base/common/network.js';
const NATIVE_WORKBENCH_ORIGIN = `${Schemas.vscodeFileResource}://${VSCODE_AUTHORITY}`;
export function getRemoteResourceResponseHeaders(requestOrigin: string | undefined, isAllowedWebEndpointOrigin: (origin: string) => boolean): Record<string, string> {
const headers: Record<string, string> = {
'Content-Security-Policy': `default-src 'none'; sandbox`,
'X-Content-Type-Options': 'nosniff',
'Vary': 'Origin',
};
if (requestOrigin && (requestOrigin === NATIVE_WORKBENCH_ORIGIN || isAllowedWebEndpointOrigin(requestOrigin))) {
headers['Access-Control-Allow-Origin'] = requestOrigin;
}
return headers;
}
@@ -0,0 +1,31 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js';
import { getRemoteResourceResponseHeaders } from '../../node/remoteResourceResponse.js';
suite('Remote resource response', () => {
ensureNoDisposablesAreLeakedInTestSuite();
test('allows trusted workbench and configured web endpoint origins', () => {
const allowedWebEndpointOrigin = 'https://example.vscode-cdn.net';
const getHeaders = (origin: string | undefined) => getRemoteResourceResponseHeaders(origin, candidate => candidate === allowedWebEndpointOrigin);
assert.deepStrictEqual({
nativeWorkbench: getHeaders('vscode-file://vscode-app')['Access-Control-Allow-Origin'],
webEndpoint: getHeaders(allowedWebEndpointOrigin)['Access-Control-Allow-Origin'],
webview: getHeaders('vscode-webview://01234567-89ab-cdef-0123-456789abcdef')['Access-Control-Allow-Origin'],
untrusted: getHeaders('https://example.com')['Access-Control-Allow-Origin'],
vary: getHeaders(undefined).Vary,
}, {
nativeWorkbench: 'vscode-file://vscode-app',
webEndpoint: allowedWebEndpointOrigin,
webview: undefined,
untrusted: undefined,
vary: 'Origin',
});
});
});
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Remote Resource Test</title>
</head>
<body></body>
</html>
+68 -2
View File
@@ -10,10 +10,11 @@
// come before any mocha imports.
process.env.MOCHA_COLORS = '1';
const { app, BrowserWindow, ipcMain, crashReporter, net: electronNet, session } = require('electron');
const { app, BrowserWindow, ipcMain, crashReporter, net: electronNet, protocol, session } = require('electron');
const product = require('../../../product.json');
const { tmpdir } = require('os');
const { existsSync, mkdirSync, promises } = require('fs');
const http = require('http');
const path = require('path');
const mocha = require('mocha');
const events = require('events');
@@ -25,6 +26,17 @@ const { applyReporter, importMochaReporter } = require('../reporter');
const minimist = require('minimist');
protocol.registerSchemesAsPrivileged([
{
scheme: 'vscode-file',
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true }
},
{
scheme: 'vscode-remote-resource',
privileges: { secure: true, supportFetchAPI: true, corsEnabled: true }
}
]);
/**
* @type {{
* _: string[];
@@ -234,7 +246,60 @@ class IPCRunner extends events.EventEmitter {
}
}
app.on('ready', () => {
app.on('ready', async () => {
const outDir = args.build ? 'out-build' : 'out';
const [{ createRemoteResourceRequestHandler }, { getRemoteResourceResponseHeaders }] = await Promise.all([
import(url.pathToFileURL(path.join(__dirname, `../../../${outDir}/vs/platform/protocol/electron-main/remoteResourceProtocol.js`)).href),
import(url.pathToFileURL(path.join(__dirname, `../../../${outDir}/vs/server/node/remoteResourceResponse.js`)).href),
]);
/** @type {import('http').IncomingHttpHeaders | undefined} */
let remoteResourceRequestHeaders;
const remoteResourceServer = http.createServer((request, response) => {
remoteResourceRequestHeaders = request.headers;
response.writeHead(200, {
...getRemoteResourceResponseHeaders(request.headers.origin, () => false),
'Content-Type': 'image/svg+xml',
});
response.write('<svg xmlns="http://www.w3.org/2000/svg">');
setTimeout(() => response.end('</svg>'), 10);
});
await new Promise((resolve, reject) => {
remoteResourceServer.once('error', reject);
remoteResourceServer.listen(0, '127.0.0.1', () => resolve());
});
const remoteResourceServerAddress = remoteResourceServer.address();
if (!remoteResourceServerAddress || typeof remoteResourceServerAddress === 'string') {
throw new Error('Remote resource test server did not bind to a TCP port');
}
protocol.handle('vscode-remote-resource', createRemoteResourceRequestHandler({ warn() { } }));
ipcMain.handle('vscode:test-remote-resource', async () => {
const remoteResourceTestWindow = new BrowserWindow({ show: false });
try {
const pagePath = url.pathToFileURL(path.join(__dirname, 'fixtures/remote-resource.html')).pathname;
await remoteResourceTestWindow.loadURL(`vscode-file://vscode-app${pagePath}`);
const remoteResourceUrl = `vscode-remote-resource://127.0.0.1:${remoteResourceServerAddress.port}/vscode-remote-resource`;
await remoteResourceTestWindow.webContents.executeJavaScript(`
new Promise((resolve, reject) => {
const image = new Image();
image.crossOrigin = 'anonymous';
image.onload = resolve;
image.onerror = () => reject(new Error('Remote resource image failed to load'));
image.src = ${JSON.stringify(remoteResourceUrl)};
document.body.append(image);
})
`);
return {
loaded: true,
requestHeaders: {
origin: remoteResourceRequestHeaders?.origin,
secFetchMode: remoteResourceRequestHeaders?.['sec-fetch-mode'],
},
};
} finally {
remoteResourceTestWindow.close();
}
});
// needed when loading resources from the renderer, e.g xterm.js or the encoding lib
session.defaultSession.protocol.handle('vscode-file', request => {
@@ -440,6 +505,7 @@ app.on('ready', () => {
if (!args.dev) {
ipcMain.on('all done', async () => {
await Promise.all(reporters.map(r => r.drain?.()));
await new Promise(resolve => remoteResourceServer.close(resolve));
app.exit(runner.didFail ? 1 : 0);
});
}