Skip to content

Commit ee2a0c4

Browse files
committed
Disables Git access in Restricted Mode (untrusted)
1 parent 2ff1890 commit ee2a0c4

File tree

8 files changed

+105
-35
lines changed

8 files changed

+105
-35
lines changed

Diff for: src/env/node/git/git.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type { CoreConfiguration } from '../../../constants';
99
import { GlyphChars } from '../../../constants';
1010
import type { GitCommandOptions, GitSpawnOptions } from '../../../git/commandOptions';
1111
import { GitErrorHandling } from '../../../git/commandOptions';
12-
import { StashPushError, StashPushErrorReason } from '../../../git/errors';
12+
import { StashPushError, StashPushErrorReason, WorkspaceUntrustedError } from '../../../git/errors';
1313
import type { GitDiffFilter } from '../../../git/models/diff';
1414
import { isUncommitted, isUncommittedStaged, shortenRevision } from '../../../git/models/reference';
1515
import type { GitUser } from '../../../git/models/user';
@@ -123,6 +123,8 @@ export class Git {
123123
async git(options: ExitCodeOnlyGitCommandOptions, ...args: any[]): Promise<number>;
124124
async git<T extends string | Buffer>(options: GitCommandOptions, ...args: any[]): Promise<T>;
125125
async git<T extends string | Buffer>(options: GitCommandOptions, ...args: any[]): Promise<T> {
126+
if (!workspace.isTrusted) throw new WorkspaceUntrustedError();
127+
126128
const start = hrtime();
127129

128130
const { configs, correlationKey, errors: errorHandling, encoding, ...opts } = options;
@@ -224,6 +226,8 @@ export class Git {
224226
}
225227

226228
async gitSpawn(options: GitSpawnOptions, ...args: any[]): Promise<ChildProcess> {
229+
if (!workspace.isTrusted) throw new WorkspaceUntrustedError();
230+
227231
const start = hrtime();
228232

229233
const { cancellation, configs, stdin, stdinEncoding, ...opts } = options;
@@ -1645,6 +1649,8 @@ export class Git {
16451649
? (emptyArray as [])
16461650
: [true, normalizePath(data.trimStart().replace(/[\r|\n]+$/, ''))];
16471651
} catch (ex) {
1652+
if (ex instanceof WorkspaceUntrustedError) return emptyArray as [];
1653+
16481654
const unsafeMatch =
16491655
/^fatal: detected dubious ownership in repository at '([^']+)'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(
16501656
ex.stderr,

Diff for: src/env/node/git/localGitProvider.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1088,7 +1088,7 @@ export class LocalGitProvider implements GitProvider, Disposable {
10881088
[safe, repoPath] = await this.git.rev_parse__show_toplevel(uri.fsPath);
10891089
if (safe) {
10901090
this.unsafePaths.delete(uri.fsPath);
1091-
} else {
1091+
} else if (safe === false) {
10921092
this.unsafePaths.add(uri.fsPath);
10931093
}
10941094
if (!repoPath) return undefined;

Diff for: src/git/errors.ts

+9
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,15 @@ export class StashPushError extends Error {
8181
Error.captureStackTrace?.(this, StashApplyError);
8282
}
8383
}
84+
85+
export class WorkspaceUntrustedError extends Error {
86+
constructor() {
87+
super('Unable to perform Git operations because the current workspace is untrusted');
88+
89+
Error.captureStackTrace?.(this, WorkspaceUntrustedError);
90+
}
91+
}
92+
8493
export const enum WorktreeCreateErrorReason {
8594
AlreadyCheckedOut = 1,
8695
AlreadyExists = 2,

Diff for: src/git/gitProviderService.ts

+13
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,12 @@ import type { RichRemoteProvider } from './remotes/richRemoteProvider';
8282
import type { GitSearch, SearchQuery } from './search';
8383

8484
const emptyArray = Object.freeze([]) as unknown as any[];
85+
const emptyDisposable = Object.freeze({
86+
dispose: () => {
87+
/* noop */
88+
},
89+
});
90+
8591
const maxDefaultBranchWeight = 100;
8692
const weightedDefaultBranches = new Map<string, number>([
8793
['master', maxDefaultBranchWeight],
@@ -216,6 +222,13 @@ export class GitProviderService implements Disposable {
216222
this.resetCaches('providers');
217223
this.updateContext();
218224
}),
225+
!workspace.isTrusted
226+
? workspace.onDidGrantWorkspaceTrust(() => {
227+
if (workspace.isTrusted && workspace.workspaceFolders?.length) {
228+
void this.discoverRepositories(workspace.workspaceFolders, { force: true });
229+
}
230+
})
231+
: emptyDisposable,
219232
...this.registerCommands(),
220233
);
221234

Diff for: src/webviews/apps/home/home.html

+11
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ <h1 class="alert__title">Unsafe repository</h1>
9696
</p>
9797
</div>
9898
</div>
99+
<div id="untrusted-alert" class="alert alert--info mb-0" aria-hidden="true" hidden>
100+
<h1 class="alert__title">Untrusted workspace</h1>
101+
<div class="alert__description">
102+
<p>Unable to open repositories in Restricted Mode.</p>
103+
<p class="centered">
104+
<vscode-button data-action="command:workbench.trust.manage"
105+
>Manage Workspace Trust</vscode-button
106+
>
107+
</p>
108+
</div>
109+
</div>
99110
<header-card id="header-card" image="#{webroot}/media/gitlens-logo.webp"></header-card>
100111
</header>
101112
<main class="home__main scrollable" id="main" tabindex="-1">

Diff for: src/webviews/apps/home/home.ts

+36-28
Original file line numberDiff line numberDiff line change
@@ -247,36 +247,26 @@ export class HomeApp extends App<State> {
247247
}
248248

249249
private updateNoRepo() {
250-
const { repositories } = this.state;
251-
const hasRepos = repositories.openCount > 0;
252-
const value = hasRepos ? 'true' : 'false';
253-
254-
let $el = document.getElementById('no-repo');
255-
$el?.setAttribute('aria-hidden', value);
256-
if (hasRepos) {
257-
$el?.setAttribute('hidden', value);
258-
} else {
259-
$el?.removeAttribute('hidden');
260-
}
250+
const {
251+
repositories: { openCount, hasUnsafe, trusted },
252+
} = this.state;
261253

262-
$el = document.getElementById('no-repo-alert');
263-
const showUnsafe = repositories.hasUnsafe && !hasRepos;
264-
const $unsafeEl = document.getElementById('unsafe-repo-alert');
265-
if (showUnsafe) {
266-
$el?.setAttribute('aria-hidden', 'true');
267-
$el?.setAttribute('hidden', 'true');
268-
$unsafeEl?.setAttribute('aria-hidden', 'false');
269-
$unsafeEl?.removeAttribute('hidden');
270-
} else {
271-
$unsafeEl?.setAttribute('aria-hidden', 'true');
272-
$unsafeEl?.setAttribute('hidden', 'true');
273-
$el?.setAttribute('aria-hidden', value);
274-
if (hasRepos) {
275-
$el?.setAttribute('hidden', value);
276-
} else {
277-
$el?.removeAttribute('hidden');
278-
}
254+
if (!trusted) {
255+
setElementVisibility('untrusted-alert', true);
256+
setElementVisibility('no-repo', false);
257+
setElementVisibility('no-repo-alert', false);
258+
setElementVisibility('unsafe-repo-alert', false);
259+
260+
return;
279261
}
262+
263+
setElementVisibility('untrusted-alert', false);
264+
265+
const noRepos = openCount === 0;
266+
267+
setElementVisibility('no-repo', noRepos);
268+
setElementVisibility('no-repo-alert', noRepos && !hasUnsafe);
269+
setElementVisibility('unsafe-repo-alert', hasUnsafe);
280270
}
281271

282272
private updateLayout() {
@@ -371,6 +361,24 @@ export class HomeApp extends App<State> {
371361
}
372362
}
373363

364+
function setElementVisibility(elementOrId: string | HTMLElement | null | undefined, visible: boolean) {
365+
let el;
366+
if (typeof elementOrId === 'string') {
367+
el = document.getElementById(elementOrId);
368+
} else {
369+
el = elementOrId;
370+
}
371+
if (el == null) return;
372+
373+
if (visible) {
374+
el.setAttribute('aria-hidden', 'false');
375+
el.removeAttribute('hidden');
376+
} else {
377+
el.setAttribute('aria-hidden', 'true');
378+
el?.setAttribute('hidden', 'true');
379+
}
380+
}
381+
374382
function toggleArrayItem(list: string[] = [], item: string, add = true) {
375383
const hasStep = list.includes(item);
376384
if (!hasStep && add) {

Diff for: src/webviews/home/homeWebview.ts

+27-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { ConfigurationChangeEvent } from 'vscode';
2-
import { Disposable, window } from 'vscode';
2+
import { Disposable, window, workspace } from 'vscode';
33
import { getAvatarUriFromGravatarEmail } from '../../avatars';
44
import { ViewsLayout } from '../../commands/setViewsLayout';
55
import type { Container } from '../../container';
@@ -10,11 +10,18 @@ import { executeCoreCommand, registerCommand } from '../../system/command';
1010
import { configuration } from '../../system/configuration';
1111
import type { Deferrable } from '../../system/function';
1212
import { debounce } from '../../system/function';
13+
import { getSettledValue } from '../../system/promise';
1314
import type { StorageChangeEvent } from '../../system/storage';
1415
import type { IpcMessage } from '../protocol';
1516
import { onIpc } from '../protocol';
1617
import type { WebviewController, WebviewProvider } from '../webviewController';
17-
import type { CompleteStepParams, DismissBannerParams, DismissSectionParams, State } from './protocol';
18+
import type {
19+
CompleteStepParams,
20+
DidChangeRepositoriesParams,
21+
DismissBannerParams,
22+
DismissSectionParams,
23+
State,
24+
} from './protocol';
1825
import {
1926
CompletedActions,
2027
CompleteStepCommandType,
@@ -27,6 +34,12 @@ import {
2734
DismissStatusCommandType,
2835
} from './protocol';
2936

37+
const emptyDisposable = Object.freeze({
38+
dispose: () => {
39+
/* noop */
40+
},
41+
});
42+
3043
export class HomeWebviewProvider implements WebviewProvider<State> {
3144
private readonly _disposable: Disposable;
3245

@@ -36,6 +49,9 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
3649
this.container.git.onDidChangeRepositories(this.onRepositoriesChanged, this),
3750
configuration.onDidChange(this.onConfigurationChanged, this),
3851
this.container.storage.onDidChange(this.onStorageChanged, this),
52+
!workspace.isTrusted
53+
? workspace.onDidGrantWorkspaceTrust(this.notifyDidChangeRepositories, this)
54+
: emptyDisposable,
3955
);
4056
}
4157

@@ -221,7 +237,12 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
221237
}
222238

223239
private async getState(subscription?: Subscription): Promise<State> {
224-
const sub = await this.getSubscription(subscription);
240+
const [visibilityResult, subscriptionResult] = await Promise.allSettled([
241+
this.getRepoVisibility(),
242+
this.getSubscription(subscription),
243+
]);
244+
245+
const sub = getSettledValue(subscriptionResult)!;
225246
const steps = this.container.storage.get('home:steps:completed', []);
226247
const sections = this.container.storage.get('home:sections:dismissed', []);
227248
const dismissedBanners = this.container.storage.get('home:banners:dismissed', []);
@@ -233,7 +254,7 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
233254
subscription: sub.subscription,
234255
completedActions: sub.completedActions,
235256
plusEnabled: this.getPlusEnabled(),
236-
visibility: await this.getRepoVisibility(),
257+
visibility: getSettledValue(visibilityResult)!,
237258
completedSteps: steps,
238259
dismissedSections: sections,
239260
avatar: sub.avatar,
@@ -255,11 +276,12 @@ export class HomeWebviewProvider implements WebviewProvider<State> {
255276
});
256277
}
257278

258-
private getRepositoriesState() {
279+
private getRepositoriesState(): DidChangeRepositoriesParams {
259280
return {
260281
count: this.container.git.repositoryCount,
261282
openCount: this.container.git.openRepositoryCount,
262283
hasUnsafe: this.container.git.hasUnsafeRepositories(),
284+
trusted: workspace.isTrusted,
263285
};
264286
}
265287

Diff for: src/webviews/home/protocol.ts

+1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export interface DidChangeRepositoriesParams {
5757
count: number;
5858
openCount: number;
5959
hasUnsafe: boolean;
60+
trusted: boolean;
6061
}
6162
export const DidChangeRepositoriesType = new IpcNotificationType<DidChangeRepositoriesParams>('repositories/didChange');
6263

0 commit comments

Comments
 (0)