Skip to content

Commit d2e1615

Browse files
committed
fix(editor): execute oxc.path.server in win32 with shell, when its the node_module binary
1 parent d52cba6 commit d2e1615

File tree

3 files changed

+58
-14
lines changed

3 files changed

+58
-14
lines changed

editors/vscode/client/PathValidator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*
99
* The check for malicious characters is not needed, but it's an additional layer of security.
1010
* When using `shell: true` in `LanguageClient.ServerOptions`, it can be vulnerable to command injection.
11-
* Even though we are not using `shell: true`, it's a good practice to validate the input.
11+
* We are using `shell: true` only on Windows when the paths ends with `node_modules/.bin/oxc_language_server`.
1212
*/
1313
export function validateSafeBinaryPath(binary: string): boolean {
1414
// Check for path traversal (including Windows variants)

editors/vscode/client/extension.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ export async function activate(context: ExtensionContext) {
137137
outputChannel,
138138
);
139139

140-
async function findBinary(): Promise<string> {
141-
let bin = configService.getUserServerBinPath();
140+
async function findBinary(bin: string | undefined): Promise<string> {
142141
if (workspace.isTrusted && bin) {
143142
try {
144143
await fsPromises.access(bin);
@@ -155,10 +154,20 @@ export async function activate(context: ExtensionContext) {
155154
);
156155
}
157156

158-
const command = await findBinary();
157+
const userDefinedBinary = configService.getUserServerBinPath();
158+
const command = await findBinary(userDefinedBinary);
159159
const run: Executable = {
160160
command: command!,
161161
options: {
162+
// On Windows we need to run the binary in a shell to be able to execute the shell npm bin script.
163+
// This is only needed when the user explicitly configures the binary to point to the npm bin script.
164+
// The extension is shipped with the `.exe` file, we don't need to run it in a shell.
165+
// Searching for the right `.exe` file inside `node_modules/` is not reliable as it depends on
166+
// the package manager used (npm, yarn, pnpm, etc) and the package version.
167+
// The npm bin script is a shell script that points to the actual binary.
168+
// Security: We validated the userDefinedBinary in `configService.getUserServerBinPath()`.
169+
shell: process.platform === 'win32' && command === userDefinedBinary &&
170+
userDefinedBinary?.endsWith('node_modules\\.bin\\oxc_language_server'),
162171
env: {
163172
...process.env,
164173
RUST_LOG: process.env.RUST_LOG || 'info',

editors/vscode/tests/ConfigService.spec.ts

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,55 @@ suite('ConfigService', () => {
1818
await Promise.all(keys.map(key => conf.update(key, undefined)));
1919
});
2020

21-
testSingleFolderMode('resolves relative server path with workspace folder', async () => {
22-
const service = new ConfigService();
23-
const nonDefinedServerPath = service.getUserServerBinPath();
21+
const getWorkspaceFolderPlatformSafe = () => {
22+
let workspace_path = WORKSPACE_FOLDER.uri.path;
23+
if (process.platform === 'win32') {
24+
workspace_path = workspace_path.replaceAll('/', '\\');
25+
if (workspace_path.startsWith('\\')) {
26+
workspace_path = workspace_path.slice(1);
27+
}
28+
}
29+
return workspace_path;
30+
};
2431

25-
strictEqual(nonDefinedServerPath, undefined);
32+
suite('getUserServerBinPath', () => {
33+
testSingleFolderMode('resolves relative server path with workspace folder', async () => {
34+
const service = new ConfigService();
35+
const nonDefinedServerPath = service.getUserServerBinPath();
2636

27-
await conf.update('path.server', '/absolute/oxc_language_server');
28-
const absoluteServerPath = service.getUserServerBinPath();
37+
strictEqual(nonDefinedServerPath, undefined);
2938

30-
strictEqual(absoluteServerPath, '/absolute/oxc_language_server');
39+
await conf.update('path.server', '/absolute/oxc_language_server');
40+
const absoluteServerPath = service.getUserServerBinPath();
3141

32-
await conf.update('path.server', './relative/oxc_language_server');
33-
const relativeServerPath = service.getUserServerBinPath();
42+
strictEqual(absoluteServerPath, '/absolute/oxc_language_server');
3443

35-
strictEqual(relativeServerPath, WORKSPACE_FOLDER.uri.path + '/relative/oxc_language_server');
44+
await conf.update('path.server', './relative/oxc_language_server');
45+
const relativeServerPath = service.getUserServerBinPath();
46+
47+
let workspace_path = getWorkspaceFolderPlatformSafe();
48+
strictEqual(relativeServerPath, `${workspace_path}/relative/oxc_language_server`);
49+
});
50+
51+
testSingleFolderMode('returns undefined for unsafe server path', async () => {
52+
const service = new ConfigService();
53+
await conf.update('path.server', '../unsafe/oxc_language_server');
54+
const unsafeServerPath = service.getUserServerBinPath();
55+
56+
strictEqual(unsafeServerPath, undefined);
57+
});
58+
59+
testSingleFolderMode('returns backslashes path on Windows', async () => {
60+
if (process.platform !== 'win32') {
61+
return;
62+
}
63+
const service = new ConfigService();
64+
await conf.update('path.server', './relative/oxc_language_server');
65+
const relativeServerPath = service.getUserServerBinPath();
66+
let workspace_path = getWorkspaceFolderPlatformSafe();
67+
68+
strictEqual(workspace_path[1], ':', 'The test workspace folder must be an absolute path with a drive letter on Windows');
69+
strictEqual(relativeServerPath, `${workspace_path}\\relative\\oxc_language_server`);
70+
});
3671
});
3772
});

0 commit comments

Comments
 (0)