Enable sandbox, contextIsolation and vscode-file for process explorer and issue reporter (#111304)

* Enable sandbox for issue reporter and process explorer (fix #101834)

* sandbox - enable vscode-file protocol for sandboxed renderers

* issues - stop setting nodeCachedDataDir

* address feedback and also use vscode-file for shared process if sandbox is on
This commit is contained in:
Benjamin Pasero 2020-11-25 16:46:03 +01:00 committed by GitHub
parent 545332f793
commit 37e9cceddc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 254 additions and 38 deletions

View file

@ -88,9 +88,14 @@
window['MonacoEnvironment'] = {};
const baseUrl = sandbox ?
`${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32', scheme: 'vscode-file', fallbackAuthority: 'vscode-app' })}/out` :
`${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32' })}/out`;
const loaderConfig = {
baseUrl: `${bootstrapLib.fileUriFromPath(configuration.appRoot, { isWindows: safeProcess.platform === 'win32' })}/out`,
'vs/nls': nlsConfig
baseUrl,
'vs/nls': nlsConfig,
preferScriptTags: sandbox
};
// Enable loading of node modules:

View file

@ -134,6 +134,15 @@ protocol.registerSchemesAsPrivileged([
corsEnabled: true,
}
},
{
scheme: 'vscode-file',
privileges: {
secure: true,
standard: true,
supportFetchAPI: true,
corsEnabled: true
}
}
]);
// Global app listeners

View file

@ -78,6 +78,12 @@ export namespace Schemas {
* Scheme used for extension pages
*/
export const extension = 'extension';
/**
* Scheme used as a replacement of `file` scheme to load
* files with our custom protocol handler (desktop only).
*/
export const vscodeFileResource = 'vscode-file';
}
class RemoteAuthoritiesImpl {
@ -132,6 +138,8 @@ export const RemoteAuthorities = new RemoteAuthoritiesImpl();
class FileAccessImpl {
private readonly FALLBACK_AUTHORITY = 'vscode-app';
/**
* Returns a URI to use in contexts where the browser is responsible
* for loading (e.g. fetch()) or when used within the DOM.
@ -143,13 +151,43 @@ class FileAccessImpl {
asBrowserUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI {
const uri = this.toUri(uriOrModule, moduleIdToUrl);
// Handle remote URIs via `RemoteAuthorities`
if (uri.scheme === Schemas.vscodeRemote) {
return RemoteAuthorities.rewrite(uri);
}
// Only convert the URI if we are in a native context and it has `file:` scheme
if (platform.isElectronSandboxed && platform.isNative && uri.scheme === Schemas.file) {
return this.toCodeFileUri(uri);
}
return uri;
}
/**
* TODO@bpasero remove me eventually when vscode-file is adopted everywhere
*/
_asCodeFileUri(uri: URI): URI;
_asCodeFileUri(moduleId: string, moduleIdToUrl: { toUrl(moduleId: string): string }): URI;
_asCodeFileUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI {
const uri = this.toUri(uriOrModule, moduleIdToUrl);
return this.toCodeFileUri(uri);
}
private toCodeFileUri(uri: URI): URI {
return uri.with({
scheme: Schemas.vscodeFileResource,
// We need to provide an authority here so that it can serve
// as origin for network and loading matters in chromium.
// If the URI is not coming with an authority already, we
// add our own
authority: uri.authority || this.FALLBACK_AUTHORITY,
query: null,
fragment: null
});
}
/**
* Returns the `file` URI to use in contexts where node.js
* is responsible for loading.
@ -159,6 +197,19 @@ class FileAccessImpl {
asFileUri(uriOrModule: URI | string, moduleIdToUrl?: { toUrl(moduleId: string): string }): URI {
const uri = this.toUri(uriOrModule, moduleIdToUrl);
// Only convert the URI if it is `vscode-file:` scheme
if (uri.scheme === Schemas.vscodeFileResource) {
return uri.with({
scheme: Schemas.file,
// Only preserve the `authority` if it is different from
// our fallback authority. This ensures we properly preserve
// Windows UNC paths that come with their own authority.
authority: uri.authority !== this.FALLBACK_AUTHORITY ? uri.authority : null,
query: null,
fragment: null
});
}
return uri;
}

View file

@ -33,6 +33,7 @@ export interface INodeProcess {
versions?: {
electron?: string;
};
sandboxed?: boolean; // Electron
type?: string;
cwd(): string;
}
@ -59,6 +60,7 @@ if (typeof process !== 'undefined') {
}
const isElectronRenderer = typeof nodeProcess?.versions?.electron === 'string' && nodeProcess.type === 'renderer';
export const isElectronSandboxed = isElectronRenderer && nodeProcess?.sandboxed;
// Web environment
if (typeof navigator === 'object' && !isElectronRenderer) {

View file

@ -0,0 +1,70 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { URI } from 'vs/base/common/uri';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { isEqual } from 'vs/base/common/resources';
import { isElectronSandboxed } from 'vs/base/common/platform';
suite('network', () => {
const enableTest = isElectronSandboxed;
(!enableTest ? test.skip : test)('FileAccess: URI (native)', () => {
// asCodeUri() & asFileUri(): simple, without authority
let originalFileUri = URI.file('network.test.ts');
let browserUri = FileAccess.asBrowserUri(originalFileUri);
assert.ok(browserUri.authority.length > 0);
let fileUri = FileAccess.asFileUri(browserUri);
assert.equal(fileUri.authority.length, 0);
assert(isEqual(originalFileUri, fileUri));
// asCodeUri() & asFileUri(): with authority
originalFileUri = URI.file('network.test.ts').with({ authority: 'test-authority' });
browserUri = FileAccess.asBrowserUri(originalFileUri);
assert.equal(browserUri.authority, originalFileUri.authority);
fileUri = FileAccess.asFileUri(browserUri);
assert(isEqual(originalFileUri, fileUri));
});
(!enableTest ? test.skip : test)('FileAccess: moduleId (native)', () => {
const browserUri = FileAccess.asBrowserUri('vs/base/test/node/network.test', require);
assert.equal(browserUri.scheme, Schemas.vscodeFileResource);
const fileUri = FileAccess.asFileUri('vs/base/test/node/network.test', require);
assert.equal(fileUri.scheme, Schemas.file);
});
(!enableTest ? test.skip : test)('FileAccess: query and fragment is dropped (native)', () => {
let originalFileUri = URI.file('network.test.ts').with({ query: 'foo=bar', fragment: 'something' });
let browserUri = FileAccess.asBrowserUri(originalFileUri);
assert.equal(browserUri.query, '');
assert.equal(browserUri.fragment, '');
});
(!enableTest ? test.skip : test)('FileAccess: query and fragment is kept if URI is already of same scheme (native)', () => {
let originalFileUri = URI.file('network.test.ts').with({ query: 'foo=bar', fragment: 'something' });
let browserUri = FileAccess.asBrowserUri(originalFileUri.with({ scheme: Schemas.vscodeFileResource }));
assert.equal(browserUri.query, 'foo=bar');
assert.equal(browserUri.fragment, 'something');
let fileUri = FileAccess.asFileUri(originalFileUri);
assert.equal(fileUri.query, 'foo=bar');
assert.equal(fileUri.fragment, 'something');
});
(!enableTest ? test.skip : test)('FileAccess: web', () => {
const originalHttpsUri = URI.file('network.test.ts').with({ scheme: 'https' });
const browserUri = FileAccess.asBrowserUri(originalHttpsUri);
assert.equal(originalHttpsUri.toString(), browserUri.toString());
});
test('FileAccess: remote URIs', () => {
const originalRemoteUri = URI.file('network.test.ts').with({ scheme: Schemas.vscodeRemote });
const browserUri = FileAccess.asBrowserUri(originalRemoteUri);
assert.notEqual(originalRemoteUri.scheme, browserUri.scheme);
});
});

View file

@ -35,6 +35,7 @@ import { getDelayedChannel, StaticRouter, createChannelReceiver, createChannelSe
import product from 'vs/platform/product/common/product';
import { ProxyAuthHandler } from 'vs/code/electron-main/auth';
import { ProxyAuthHandler2 } from 'vs/code/electron-main/auth2';
import { FileProtocolHandler } from 'vs/code/electron-main/protocol';
import { Disposable } from 'vs/base/common/lifecycle';
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
import { URI } from 'vs/base/common/uri';
@ -412,6 +413,9 @@ export class CodeApplication extends Disposable {
this.logService.error(error);
}
// Setup Protocol Handler
const fileProtocolHandler = this._register(this.instantiationService.createInstance(FileProtocolHandler));
// Create Electron IPC Server
const electronIpcServer = new ElectronIPCServer();
@ -457,7 +461,7 @@ export class CodeApplication extends Disposable {
}
// Open Windows
const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));
const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient, fileProtocolHandler));
// Post Open Windows Tasks
appInstantiationService.invokeFunction(accessor => this.afterWindowOpen(accessor));
@ -583,7 +587,7 @@ export class CodeApplication extends Disposable {
});
}
private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>): ICodeWindow[] {
private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>, fileProtocolHandler: FileProtocolHandler): ICodeWindow[] {
// Register more Main IPC services
const launchMainService = accessor.get(ILaunchMainService);
@ -645,8 +649,10 @@ export class CodeApplication extends Disposable {
electronIpcServer.registerChannel('logger', loggerChannel);
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
// ExtensionHost Debug broadcast service
const windowsMainService = this.windowsMainService = accessor.get(IWindowsMainService);
fileProtocolHandler.injectWindowsMainService(windowsMainService);
// ExtensionHost Debug broadcast service
electronIpcServer.registerChannel(ExtensionHostDebugBroadcastChannel.ChannelName, new ElectronExtensionHostDebugBroadcastChannel(windowsMainService));
// Signal phase: ready (services set)

View file

@ -0,0 +1,93 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { FileAccess, Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { session } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
import { TernarySearchTree } from 'vs/base/common/map';
import { isLinux } from 'vs/base/common/platform';
import { IWindowsMainService } from 'vs/platform/windows/electron-main/windows';
type ProtocolCallback = { (result: string | Electron.FilePathWithHeaders | { error: number }): void };
export class FileProtocolHandler extends Disposable {
private readonly validRoots = TernarySearchTree.forUris<boolean>(() => !isLinux);
constructor(
@INativeEnvironmentService environmentService: INativeEnvironmentService,
@ILogService private readonly logService: ILogService
) {
super();
const { defaultSession } = session;
// Define an initial set of roots we allow loading from
// - appRoot : all files installed as part of the app
// - extensions : all files shipped from extensions
this.validRoots.set(URI.file(environmentService.appRoot), true);
this.validRoots.set(URI.file(environmentService.extensionsPath), true);
// Register vscode-file:// handler
defaultSession.protocol.registerFileProtocol(Schemas.vscodeFileResource, (request, callback) => this.handleResourceRequest(request, callback as unknown as ProtocolCallback));
// Cleanup
this._register(toDisposable(() => {
defaultSession.protocol.unregisterProtocol(Schemas.vscodeFileResource);
}));
}
injectWindowsMainService(windowsMainService: IWindowsMainService): void {
this._register(windowsMainService.onWindowReady(window => {
if (window.config?.extensionDevelopmentPath || window.config?.extensionTestsPath) {
const disposables = new DisposableStore();
disposables.add(Event.any(window.onClose, window.onDestroy)(() => disposables.dispose()));
// Allow access to extension development path
if (window.config.extensionDevelopmentPath) {
for (const extensionDevelopmentPath of window.config.extensionDevelopmentPath) {
disposables.add(this.addValidRoot(URI.file(extensionDevelopmentPath)));
}
}
// Allow access to extension tests path
if (window.config.extensionTestsPath) {
disposables.add(this.addValidRoot(URI.file(window.config.extensionTestsPath)));
}
}
}));
}
private addValidRoot(root: URI): IDisposable {
if (!this.validRoots.get(root)) {
this.validRoots.set(root, true);
return toDisposable(() => this.validRoots.delete(root));
}
return Disposable.None;
}
private async handleResourceRequest(request: Electron.ProtocolRequest, callback: ProtocolCallback) {
const uri = URI.parse(request.url);
// Restore the `vscode-file` URI to a `file` URI so that we can
// ensure the root is valid and properly tell Chrome where the
// resource is at.
const fileUri = FileAccess.asFileUri(uri);
if (this.validRoots.findSubstr(fileUri)) {
return callback({
path: fileUri.fsPath
});
}
this.logService.error(`${Schemas.vscodeFileResource}: Refused to load resource ${fileUri.fsPath}}`);
callback({ error: -3 /* ABORTED */ });
}
}

View file

@ -60,8 +60,9 @@ export class SharedProcess implements ISharedProcess {
windowId: this.window.id
};
const windowUrl = FileAccess
.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require)
const windowUrl = (this.environmentService.sandbox ?
FileAccess._asCodeFileUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require) :
FileAccess.asBrowserUri('vs/code/electron-browser/sharedProcess/sharedProcess.html', require))
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` });
this.window.loadURL(windowUrl.toString(true));

View file

@ -848,10 +848,11 @@ export class CodeWindow extends Disposable implements ICodeWindow {
workbench = 'vs/code/electron-browser/workbench/workbench.html';
}
return FileAccess
.asBrowserUri(workbench, require)
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` })
.toString(true);
const browserUri = this.environmentService.sandbox ?
FileAccess._asCodeFileUri(workbench, require) :
FileAccess.asBrowserUri(workbench, require);
return browserUri.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` }).toString(true);
}
serializeWindowState(): IWindowState {

View file

@ -209,18 +209,8 @@ export class IssueMainService implements ICommonIssueService {
spellcheck: false,
nativeWindowOpen: true,
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
...this.environmentService.sandbox ?
// Sandbox
{
sandbox: true,
contextIsolation: true
} :
// No Sandbox
{
nodeIntegration: true
}
sandbox: true,
contextIsolation: true
}
});
@ -275,18 +265,8 @@ export class IssueMainService implements ICommonIssueService {
spellcheck: false,
nativeWindowOpen: true,
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
...this.environmentService.sandbox ?
// Sandbox
{
sandbox: true,
contextIsolation: true
} :
// No Sandbox
{
nodeIntegration: true
}
sandbox: true,
contextIsolation: true
}
});
@ -294,7 +274,6 @@ export class IssueMainService implements ICommonIssueService {
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
nodeCachedDataDir: this.environmentService.nodeCachedDataDir,
windowId: this._processExplorerWindow.id,
userEnv: this.userEnv,
machineId: this.machineId,
@ -422,7 +401,6 @@ export class IssueMainService implements ICommonIssueService {
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
nodeCachedDataDir: this.environmentService.nodeCachedDataDir,
windowId: this._issueWindow.id,
machineId: this.machineId,
userEnv: this.userEnv,
@ -458,7 +436,7 @@ function toWindowUrl<T>(modulePathToHtml: string, windowConfiguration: T): strin
}
return FileAccess
.asBrowserUri(modulePathToHtml, require)
._asCodeFileUri(modulePathToHtml, require)
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` })
.toString(true);
}