Skip to content

Commit 395bfe1

Browse files
authored
Prompt when workspace overrides clangd.path and clangd.arguments (#160)
Prompt when workspace overrides clangd.path and clangd.arguments
1 parent a9653a8 commit 395bfe1

6 files changed

+123
-12
lines changed

docs/settings.md

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Settings
2+
3+
## Security
4+
5+
This extension allows configuring command-line flags to the `clangd` executable.
6+
7+
Being able to configure these per-workspace can be convenient, but runs the risk
8+
of an attacker changing your flags if you happen to open their repository.
9+
Similarly, allowing the workspace to control the `clangd.path` setting is risky.
10+
11+
When these flags are configured in the workspace, the extension will prompt you
12+
whether to use them or not. If you're unsure, click "No".
13+
14+
![Screenshot: Workspace trying to override clangd.path](workspace-override.png)
15+
16+
If you choose "Yes" or "No", the answer will be remembered for this workspace.
17+
(If the value changes, you will be prompted again).

docs/workspace-override.png

13.7 KB
Loading

src/clangd-context.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,17 @@ export class ClangdContext implements vscode.Disposable {
4545
subscriptions: vscode.Disposable[] = [];
4646
client: ClangdLanguageClient;
4747

48-
async activate(globalStoragePath: string,
49-
outputChannel: vscode.OutputChannel) {
50-
const clangdPath = await install.activate(this, globalStoragePath);
48+
async activate(globalStoragePath: string, outputChannel: vscode.OutputChannel,
49+
workspaceState: vscode.Memento) {
50+
const clangdPath =
51+
await install.activate(this, globalStoragePath, workspaceState);
5152
if (!clangdPath)
5253
return;
5354

5455
const clangd: vscodelc.Executable = {
5556
command: clangdPath,
56-
args: config.get<string[]>('arguments'),
57+
args:
58+
await config.getSecureOrPrompt<string[]>('arguments', workspaceState),
5759
options: {cwd: vscode.workspace.rootPath || process.cwd()}
5860
};
5961
const traceFile = config.get<string>('trace');

src/config.ts

+86
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,47 @@ export function get<T>(key: string): T {
66
return substitute(vscode.workspace.getConfiguration('clangd').get(key));
77
}
88

9+
// Like get(), but won't load settings from workspace config unless the user has
10+
// previously explicitly allowed this.
11+
export function getSecure<T>(key: string, workspaceState: vscode.Memento): T|
12+
undefined {
13+
const prop = new SecureProperty<T>(key, workspaceState);
14+
return prop.get(prop.blessed ?? false);
15+
}
16+
17+
// Like get(), but won't implicitly load settings from workspace config.
18+
// If there is workspace config, prompts the user and caches the decision.
19+
export async function getSecureOrPrompt<T>(
20+
key: string, workspaceState: vscode.Memento): Promise<T> {
21+
const prop = new SecureProperty<T>(key, workspaceState);
22+
// Common case: value not overridden in workspace.
23+
if (!prop.mismatched)
24+
return prop.get(false);
25+
// Check whether user has blessed or blocked this value.
26+
const blessed = prop.blessed;
27+
if (blessed !== null)
28+
return prop.get(blessed);
29+
// No cached decision for this value, ask the user.
30+
const Yes = 'Yes, use this setting', No = 'No, use my default',
31+
Info = 'More Info'
32+
switch (await vscode.window.showWarningMessage(
33+
`This workspace wants to set clangd.${key} to ${prop.insecureJSON}.
34+
\u2029
35+
This will override your default of ${prop.secureJSON}.`,
36+
Yes, No, Info)) {
37+
case Info:
38+
vscode.env.openExternal(vscode.Uri.parse(
39+
'https://github.com/clangd/vscode-clangd/blob/master/docs/settings.md#security'));
40+
break;
41+
case Yes:
42+
await prop.bless(true);
43+
return prop.get(true);
44+
case No:
45+
await prop.bless(false);
46+
}
47+
return prop.get(false);
48+
}
49+
950
// Sets the config value `clangd.<key>`. Does not apply substitutions.
1051
export function update<T>(key: string, value: T,
1152
target?: vscode.ConfigurationTarget) {
@@ -54,3 +95,48 @@ function replacement(name: string): string|null {
5495

5596
return null;
5697
}
98+
99+
// Caches a user's decision about whether to respect a workspace override of a
100+
// sensitive setting. Valid only for a particular variable, workspace, and
101+
// value.
102+
interface BlessCache {
103+
json: string // JSON-serialized workspace value that the decision governs.
104+
allowed: boolean // Whether the user chose to allow this value.
105+
}
106+
107+
// Common logic between getSecure and getSecureOrPrompt.
108+
class SecureProperty<T> {
109+
secure: T|undefined;
110+
insecure: T|undefined;
111+
public secureJSON: string;
112+
public insecureJSON: string;
113+
blessKey: string;
114+
115+
constructor(key: string, private workspaceState: vscode.Memento) {
116+
const cfg = vscode.workspace.getConfiguration('clangd');
117+
const inspect = cfg.inspect<T>(key);
118+
this.secure = inspect.globalValue ?? inspect.defaultValue;
119+
this.insecure = cfg.get<T>(key);
120+
this.secureJSON = JSON.stringify(this.secure);
121+
this.insecureJSON = JSON.stringify(this.insecure);
122+
this.blessKey = 'bless.' + key;
123+
}
124+
125+
get mismatched(): boolean { return this.secureJSON != this.insecureJSON; }
126+
127+
get(trusted: boolean): T|undefined {
128+
return substitute(trusted ? this.insecure : this.secure);
129+
}
130+
131+
get blessed(): boolean|null {
132+
let cache = this.workspaceState.get<BlessCache>(this.blessKey);
133+
if (!cache || cache.json != this.insecureJSON)
134+
return null;
135+
return cache.allowed;
136+
}
137+
138+
async bless(b: boolean): Promise<void> {
139+
await this.workspaceState.update(this.blessKey,
140+
{json: this.insecureJSON, allowed: b});
141+
}
142+
}

src/extension.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ export async function activate(context: vscode.ExtensionContext) {
2020
context.subscriptions.push(
2121
vscode.commands.registerCommand('clangd.restart', async () => {
2222
await clangdContext.dispose();
23-
await clangdContext.activate(context.globalStoragePath, outputChannel);
23+
await clangdContext.activate(context.globalStoragePath, outputChannel,
24+
context.workspaceState);
2425
}));
2526

26-
await clangdContext.activate(context.globalStoragePath, outputChannel);
27+
await clangdContext.activate(context.globalStoragePath, outputChannel,
28+
context.workspaceState);
2729

2830
setInterval(function() {
2931
const cppTools = vscode.extensions.getExtension('ms-vscode.cpptools');

src/install.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@ import {ClangdContext} from './clangd-context';
1010
import * as config from './config';
1111

1212
// Returns the clangd path to be used, or null if clangd is not installed.
13-
export async function activate(context: ClangdContext,
14-
globalStoragePath: string): Promise<string> {
15-
const ui = new UI(context, globalStoragePath);
13+
export async function activate(
14+
context: ClangdContext, globalStoragePath: string,
15+
workspaceState: vscode.Memento): Promise<string> {
16+
// If the workspace overrides clangd.path, give the user a chance to bless it.
17+
await config.getSecureOrPrompt<string>('path', workspaceState);
18+
19+
const ui = new UI(context, globalStoragePath, workspaceState);
1620
context.subscriptions.push(vscode.commands.registerCommand(
1721
'clangd.install', async () => common.installLatest(ui)));
1822
context.subscriptions.push(vscode.commands.registerCommand(
@@ -22,8 +26,8 @@ export async function activate(context: ClangdContext,
2226
}
2327

2428
class UI {
25-
constructor(private context: ClangdContext,
26-
private globalStoragePath: string) {}
29+
constructor(private context: ClangdContext, private globalStoragePath: string,
30+
private workspaceState: vscode.Memento) {}
2731

2832
get storagePath(): string { return this.globalStoragePath; }
2933
async choose(prompt: string, options: string[]): Promise<string|undefined> {
@@ -122,7 +126,7 @@ class UI {
122126
}
123127

124128
get clangdPath(): string {
125-
let p = config.get<string>('path');
129+
let p = config.getSecure<string>('path', this.workspaceState);
126130
// Backwards compatibility: if it's a relative path with a slash, interpret
127131
// relative to project root.
128132
if (!path.isAbsolute(p) && p.indexOf(path.sep) != -1 &&

0 commit comments

Comments
 (0)