diff --git a/src/vs/code/node/cli.ts b/src/vs/code/node/cli.ts index b25377a804d32..62682daa16965 100644 --- a/src/vs/code/node/cli.ts +++ b/src/vs/code/node/cli.ts @@ -125,14 +125,26 @@ export async function main(argv: string[]): Promise { // Write File else if (args['file-write']) { - const source = args._[0]; - const target = args._[1]; + const argsFile = args._[0]; + if (!argsFile || !isAbsolute(argsFile) || !existsSync(argsFile) || !statSync(argsFile).isFile()) { + throw new Error('Using --file-write with invalid arguments.'); + } + + let source: string | undefined; + let target: string | undefined; + try { + const argsContents = JSON.parse(readFileSync(argsFile, 'utf8')); + source = argsContents.source; + target = argsContents.target; + } catch (error) { + throw new Error('Using --file-write with invalid arguments.'); + } // Windows: set the paths as allowed UNC paths given // they are explicitly provided by the user as arguments if (isWindows) { for (const path of [source, target]) { - if (isUNC(path)) { + if (typeof path === 'string' && isUNC(path)) { addUNCHostToAllowlist(URI.file(path).authority); } } diff --git a/src/vs/platform/native/electron-main/nativeHostMainService.ts b/src/vs/platform/native/electron-main/nativeHostMainService.ts index 6f57b9a7b44ac..ae6826ac9eeed 100644 --- a/src/vs/platform/native/electron-main/nativeHostMainService.ts +++ b/src/vs/platform/native/electron-main/nativeHostMainService.ts @@ -47,6 +47,7 @@ import { CancellationError } from '../../../base/common/errors.js'; import { IConfigurationService } from '../../configuration/common/configuration.js'; import { IProxyAuthService } from './auth.js'; import { AuthInfo, Credentials, IRequestService } from '../../request/common/request.js'; +import { randomPath } from '../../../base/common/extpath.js'; export interface INativeHostMainService extends AddFirstParameterToFunctions /* only methods, not events */, number | undefined /* window ID */> { } @@ -568,35 +569,44 @@ export class NativeHostMainService extends Disposable implements INativeHostMain async writeElevated(windowId: number | undefined, source: URI, target: URI, options?: { unlock?: boolean }): Promise { const sudoPrompt = await import('@vscode/sudo-prompt'); - return new Promise((resolve, reject) => { - const sudoCommand: string[] = [`"${this.cliPath}"`]; - if (options?.unlock) { - sudoCommand.push('--file-chmod'); - } + const argsFile = randomPath(this.environmentMainService.userDataPath, 'code-elevated'); + await Promises.writeFile(argsFile, JSON.stringify({ source: source.fsPath, target: target.fsPath })); - sudoCommand.push('--file-write', `"${source.fsPath}"`, `"${target.fsPath}"`); + try { + await new Promise((resolve, reject) => { + const sudoCommand: string[] = [`"${this.cliPath}"`]; + if (options?.unlock) { + sudoCommand.push('--file-chmod'); + } - const promptOptions = { - name: this.productService.nameLong.replace('-', ''), - icns: (isMacintosh && this.environmentMainService.isBuilt) ? join(dirname(this.environmentMainService.appRoot), `${this.productService.nameShort}.icns`) : undefined - }; + sudoCommand.push('--file-write', `"${argsFile}"`); - sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error?, stdout?, stderr?) => { - if (stdout) { - this.logService.trace(`[sudo-prompt] received stdout: ${stdout}`); - } + const promptOptions = { + name: this.productService.nameLong.replace('-', ''), + icns: (isMacintosh && this.environmentMainService.isBuilt) ? join(dirname(this.environmentMainService.appRoot), `${this.productService.nameShort}.icns`) : undefined + }; - if (stderr) { - this.logService.trace(`[sudo-prompt] received stderr: ${stderr}`); - } + this.logService.trace(`[sudo-prompt] running command: ${sudoCommand.join(' ')}`); - if (error) { - reject(error); - } else { - resolve(undefined); - } + sudoPrompt.exec(sudoCommand.join(' '), promptOptions, (error?, stdout?, stderr?) => { + if (stdout) { + this.logService.trace(`[sudo-prompt] received stdout: ${stdout}`); + } + + if (stderr) { + this.logService.error(`[sudo-prompt] received stderr: ${stderr}`); + } + + if (error) { + reject(error); + } else { + resolve(undefined); + } + }); }); - }); + } finally { + await fs.promises.unlink(argsFile); + } } async isRunningUnderARM64Translation(): Promise { diff --git a/src/vs/workbench/services/files/electron-sandbox/elevatedFileService.ts b/src/vs/workbench/services/files/electron-sandbox/elevatedFileService.ts index 09f67aba17c9f..158a5ac0f4c6a 100644 --- a/src/vs/workbench/services/files/electron-sandbox/elevatedFileService.ts +++ b/src/vs/workbench/services/files/electron-sandbox/elevatedFileService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from '../../../../nls.js'; import { VSBuffer, VSBufferReadable, VSBufferReadableStream } from '../../../../base/common/buffer.js'; import { randomPath } from '../../../../base/common/extpath.js'; import { Schemas } from '../../../../base/common/network.js'; @@ -10,9 +11,11 @@ import { URI } from '../../../../base/common/uri.js'; import { IFileService, IFileStatWithMetadata, IWriteFileOptions } from '../../../../platform/files/common/files.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; +import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js'; import { INativeWorkbenchEnvironmentService } from '../../environment/electron-sandbox/environmentService.js'; import { IElevatedFileService } from '../common/elevatedFileService.js'; - +import { isWindows } from '../../../../base/common/platform.js'; +import { ILabelService } from '../../../../platform/label/common/label.js'; export class NativeElevatedFileService implements IElevatedFileService { readonly _serviceBrand: undefined; @@ -20,7 +23,9 @@ export class NativeElevatedFileService implements IElevatedFileService { constructor( @INativeHostService private readonly nativeHostService: INativeHostService, @IFileService private readonly fileService: IFileService, - @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService + @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, + @IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService, + @ILabelService private readonly labelService: ILabelService ) { } isSupported(resource: URI): boolean { @@ -32,6 +37,13 @@ export class NativeElevatedFileService implements IElevatedFileService { } async writeFileElevated(resource: URI, value: VSBuffer | VSBufferReadable | VSBufferReadableStream, options?: IWriteFileOptions): Promise { + const trusted = await this.workspaceTrustRequestService.requestWorkspaceTrust({ + message: isWindows ? localize('fileNotTrustedMessageWindows', "You are about to save '{0}' as admin.", this.labelService.getUriLabel(resource)) : localize('fileNotTrustedMessagePosix', "You are about to save '{0}' as super user.", this.labelService.getUriLabel(resource)), + }); + if (!trusted) { + throw new Error(localize('fileNotTrusted', "Workspace is not trusted.")); + } + const source = URI.file(randomPath(this.environmentService.userDataPath, 'code-elevated')); try { // write into a tmp file first