CVE-2023-46944 (published 2023-11-28T22:15Z)

An issue in GitKraken GitLens before v.14.0.0 allows an attacker to execute arbitrary code via a crafted file to the Visual Studio Codes workspace trust component.

CVSS v3 base score: 7.8, HIGH (CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/gitkraken/vscode-gitlens
commit_details: repo_url: https://github.com/gitkraken/vscode-gitlens
author: Eric Amodio
author_date: 2023-05-17 15:58:24-04:00
msg: Disables Git access in Restricted Mode (untrusted)
repository: repo_url: https://github.com/gitkraken/vscode-gitlens
repo_name: gitkraken/vscode-gitlens
description: Supercharge Git inside VS Code and unlock untapped knowledge within each repository — Visualize code authorship at a glance via Git blame annotations and CodeLens, seamlessly navigate and explore Git repositories, gain valuable insights via rich visualizations and powerful comparison commands, and so much more
repo_language: TypeScript
owner: gitkraken
file_changes: filename: git.ts
change_type: ModificationType.MODIFY
diff: @@ -9,7 +9,7 @@ import type { CoreConfiguration } from '../../../constants';
 import { GlyphChars } from '../../../constants';
 import type { GitCommandOptions, GitSpawnOptions } from '../../../git/commandOptions';
 import { GitErrorHandling } from '../../../git/commandOptions';
-import { StashPushError, StashPushErrorReason } from '../../../git/errors';
+import { StashPushError, StashPushErrorReason, WorkspaceUntrustedError } from '../../../git/errors';
 import type { GitDiffFilter } from '../../../git/models/diff';
 import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/models/reference';
 import type { GitUser } from '../../../git/models/user';
@@ -123,6 +123,8 @@ export class Git {
 	async git(options: ExitCodeOnlyGitCommandOptions, ...args: any[]): Promise<number>;
 	async git<T extends string | Buffer>(options: GitCommandOptions, ...args: any[]): Promise<T>;
 	async git<T extends string | Buffer>(options: GitCommandOptions, ...args: any[]): Promise<T> {
+		if (!workspace.isTrusted) throw new WorkspaceUntrustedError();
+
 		const start = hrtime();
 
 		const { configs, correlationKey, errors: errorHandling, encoding, ...opts } = options;
@@ -224,6 +226,8 @@ export class Git {
 	}
 
 	async gitSpawn(options: GitSpawnOptions, ...args: any[]): Promise<ChildProcess> {
+		if (!workspace.isTrusted) throw new WorkspaceUntrustedError();
+
 		const start = hrtime();
 
 		const { cancellation, configs, stdin, stdinEncoding, ...opts } = options;
@@ -1645,6 +1649,8 @@ export class Git {
 				? (emptyArray as [])
 				: [true, normalizePath(data.trimStart().replace(/[\r|\n]+$/, ''))];
 		} catch (ex) {
+			if (ex instanceof WorkspaceUntrustedError) return emptyArray as [];
+
 			const unsafeMatch =
 				/^fatal: detected dubious ownership in repository at '([^']+)'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(
 					ex.stderr,

programming_language: TypeScript
filename: localGitProvider.ts
change_type: ModificationType.MODIFY
diff: @@ -1088,7 +1088,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
 			[safe, repoPath] = await this.git.rev_parse__show_toplevel(uri.fsPath);
 			if (safe) {
 				this.unsafePaths.delete(uri.fsPath);
-			} else {
+			} else if (safe === false) {
 				this.unsafePaths.add(uri.fsPath);
 			}
 			if (!repoPath) return undefined;

programming_language: TypeScript
filename: errors.ts
change_type: ModificationType.MODIFY
diff: @@ -81,6 +81,15 @@ export class StashPushError extends Error {
 		Error.captureStackTrace?.(this, StashApplyError);
 	}
 }
+
+export class WorkspaceUntrustedError extends Error {
+	constructor() {
+		super('Unable to perform Git operations because the current workspace is untrusted');
+
+		Error.captureStackTrace?.(this, WorkspaceUntrustedError);
+	}
+}
+
 export const enum WorktreeCreateErrorReason {
 	AlreadyCheckedOut = 1,
 	AlreadyExists = 2,

programming_language: TypeScript
filename: gitProviderService.ts
change_type: ModificationType.MODIFY
diff: @@ -82,6 +82,12 @@ import type { RichRemoteProvider } from './remotes/richRemoteProvider';
 import type { GitSearch, SearchQuery } from './search';
 
 const emptyArray = Object.freeze([]) as unknown as any[];
+const emptyDisposable = Object.freeze({
+	dispose: () => {
+		/* noop */
+	},
+});
+
 const maxDefaultBranchWeight = 100;
 const weightedDefaultBranches = new Map<string, number>([
 	['master', maxDefaultBranchWeight],
@@ -216,6 +222,13 @@ export class GitProviderService implements Disposable {
 				this.resetCaches('providers');
 				this.updateContext();
 			}),
+			!workspace.isTrusted
+				? workspace.onDidGrantWorkspaceTrust(() => {
+						if (workspace.isTrusted && workspace.workspaceFolders?.length) {
+							void this.discoverRepositories(workspace.workspaceFolders, { force: true });
+						}
+				  })
+				: emptyDisposable,
 			...this.registerCommands(),
 		);
 

programming_language: TypeScript
filename: home.html
change_type: ModificationType.MODIFY
diff: @@ -96,6 +96,17 @@
 					</p>
 				</div>
 			</div>
+			<div id="untrusted-alert" class="alert alert--info mb-0" aria-hidden="true" hidden>
+				<h1 class="alert__title">Untrusted workspace</h1>
+				<div class="alert__description">
+					<p>Unable to open repositories in Restricted Mode.</p>
+					<p class="centered">
+						<vscode-button data-action="command:workbench.trust.manage"
+							>Manage Workspace Trust</vscode-button
+						>
+					</p>
+				</div>
+			</div>
 			<header-card id="header-card" image="#{webroot}/media/gitlens-logo.webp"></header-card>
 		</header>
 		<main class="home__main scrollable" id="main" tabindex="-1">

programming_language: HTML
filename: home.ts
change_type: ModificationType.MODIFY
diff: @@ -247,36 +247,26 @@ export class HomeApp extends App<State> {
 	}
 
 	private updateNoRepo() {
-		const { repositories } = this.state;
-		const hasRepos = repositories.openCount > 0;
-		const value = hasRepos ? 'true' : 'false';
-
-		let $el = document.getElementById('no-repo');
-		$el?.setAttribute('aria-hidden', value);
-		if (hasRepos) {
-			$el?.setAttribute('hidden', value);
-		} else {
-			$el?.removeAttribute('hidden');
-		}
+		const {
+			repositories: { openCount, hasUnsafe, trusted },
+		} = this.state;
 
-		$el = document.getElementById('no-repo-alert');
-		const showUnsafe = repositories.hasUnsafe && !hasRepos;
-		const $unsafeEl = document.getElementById('unsafe-repo-alert');
-		if (showUnsafe) {
-			$el?.setAttribute('aria-hidden', 'true');
-			$el?.setAttribute('hidden', 'true');
-			$unsafeEl?.setAttribute('aria-hidden', 'false');
-			$unsafeEl?.removeAttribute('hidden');
-		} else {
-			$unsafeEl?.setAttribute('aria-hidden', 'true');
-			$unsafeEl?.setAttribute('hidden', 'true');
-			$el?.setAttribute('aria-hidden', value);
-			if (hasRepos) {
-				$el?.setAttribute('hidden', value);
-			} else {
-				$el?.removeAttribute('hidden');
-			}
+		if (!trusted) {
+			setElementVisibility('untrusted-alert', true);
+			setElementVisibility('no-repo', false);
+			setElementVisibility('no-repo-alert', false);
+			setElementVisibility('unsafe-repo-alert', false);
+
+			return;
 		}
+
+		setElementVisibility('untrusted-alert', false);
+
+		const noRepos = openCount === 0;
+
+		setElementVisibility('no-repo', noRepos);
+		setElementVisibility('no-repo-alert', noRepos && !hasUnsafe);
+		setElementVisibility('unsafe-repo-alert', hasUnsafe);
 	}
 
 	private updateLayout() {
@@ -371,6 +361,24 @@ export class HomeApp extends App<State> {
 	}
 }
 
+function setElementVisibility(elementOrId: string | HTMLElement | null | undefined, visible: boolean) {
+	let el;
+	if (typeof elementOrId === 'string') {
+		el = document.getElementById(elementOrId);
+	} else {
+		el = elementOrId;
+	}
+	if (el == null) return;
+
+	if (visible) {
+		el.setAttribute('aria-hidden', 'false');
+		el.removeAttribute('hidden');
+	} else {
+		el.setAttribute('aria-hidden', 'true');
+		el?.setAttribute('hidden', 'true');
+	}
+}
+
 function toggleArrayItem(list: string[] = [], item: string, add = true) {
 	const hasStep = list.includes(item);
 	if (!hasStep && add) {

programming_language: TypeScript
filename: homeWebview.ts
change_type: ModificationType.MODIFY
diff: @@ -1,5 +1,5 @@
 import type { ConfigurationChangeEvent } from 'vscode';
-import { Disposable, window } from 'vscode';
+import { Disposable, window, workspace } from 'vscode';
 import { getAvatarUriFromGravatarEmail } from '../../avatars';
 import { ViewsLayout } from '../../commands/setViewsLayout';
 import type { Container } from '../../container';
@@ -10,11 +10,18 @@ import { executeCoreCommand, registerCommand } from '../../system/command';
 import { configuration } from '../../system/configuration';
 import type { Deferrable } from '../../system/function';
 import { debounce } from '../../system/function';
+import { getSettledValue } from '../../system/promise';
 import type { StorageChangeEvent } from '../../system/storage';
 import type { IpcMessage } from '../protocol';
 import { onIpc } from '../protocol';
 import type { WebviewController, WebviewProvider } from '../webviewController';
-import type { CompleteStepParams, DismissBannerParams, DismissSectionParams, State } from './protocol';
+import type {
+	CompleteStepParams,
+	DidChangeRepositoriesParams,
+	DismissBannerParams,
+	DismissSectionParams,
+	State,
+} from './protocol';
 import {
 	CompletedActions,
 	CompleteStepCommandType,
@@ -27,6 +34,12 @@ import {
 	DismissStatusCommandType,
 } from './protocol';
 
+const emptyDisposable = Object.freeze({
+	dispose: () => {
+		/* noop */
+	},
+});
+
 export class HomeWebviewProvider implements WebviewProvider<State> {
 	private readonly _disposable: Disposable;
 
@@ -36,6 +49,9 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
 			this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this),
 			configuration.onDidChange(this.onConfigurationChanged, this),
 			this.container.storage.onDidChange(this.onStorageChanged, this),
+			!workspace.isTrusted
+				? workspace.onDidGrantWorkspaceTrust(this.notifyDidChangeRepositories, this)
+				: emptyDisposable,
 		);
 	}
 
@@ -221,7 +237,12 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
 	}
 
 	private async getState(subscription?: Subscription): Promise<State> {
-		const sub = await this.getSubscription(subscription);
+		const [visibilityResult, subscriptionResult] = await Promise.allSettled([
+			this.getRepoVisibility(),
+			this.getSubscription(subscription),
+		]);
+
+		const sub = getSettledValue(subscriptionResult)!;
 		const steps = this.container.storage.get('home:steps:completed', []);
 		const sections = this.container.storage.get('home:sections:dismissed', []);
 		const dismissedBanners = this.container.storage.get('home:banners:dismissed', []);
@@ -233,7 +254,7 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
 			subscription: sub.subscription,
 			completedActions: sub.completedActions,
 			plusEnabled: this.getPlusEnabled(),
-			visibility: await this.getRepoVisibility(),
+			visibility: getSettledValue(visibilityResult)!,
 			completedSteps: steps,
 			dismissedSections: sections,
 			avatar: sub.avatar,
@@ -255,11 +276,12 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
 		});
 	}
 
-	private getRepositoriesState() {
+	private getRepositoriesState(): DidChangeRepositoriesParams {
 		return {
 			count: this.container.git.repositoryCount,
 			openCount: this.container.git.openRepositoryCount,
 			hasUnsafe: this.container.git.hasUnsafeRepositories(),
+			trusted: workspace.isTrusted,
 		};
 	}
 

programming_language: TypeScript
filename: protocol.ts
change_type: ModificationType.MODIFY
diff: @@ -57,6 +57,7 @@ export interface DidChangeRepositoriesParams {
 	count: number;
 	openCount: number;
 	hasUnsafe: boolean;
+	trusted: boolean;
 }
 export const DidChangeRepositoriesType = new IpcNotificationType<DidChangeRepositoriesParams>('repositories/didChange');
 

programming_language: TypeScript