Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions editors/vscode/client/ConfigService.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as path from 'node:path';
import { ConfigurationChangeEvent, Uri, workspace, WorkspaceFolder } from 'vscode';
import { validateSafeBinaryPath } from './PathValidator';
import { IDisposable } from './types';
import { VSCodeConfig } from './VSCodeConfig';
import { WorkspaceConfig, WorkspaceConfigInterface } from './WorkspaceConfig';
Expand Down Expand Up @@ -66,6 +67,11 @@ export class ConfigService implements IDisposable {
return;
}

// validates the given path is safe to use
if (validateSafeBinaryPath(bin) === false) {
return;
}

if (!path.isAbsolute(bin)) {
// if the path is not absolute, resolve it to the first workspace folder
let cwd = this.workspaceConfigs.keys().next().value;
Expand Down
35 changes: 35 additions & 0 deletions editors/vscode/client/PathValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Validates the given project given path to ensure it is safe to use.
*
* Following checks are performed:
* Check for path traversal (e.g., using `..` to go up directories).
* Check for malicious characters or patterns (e.g., `$`, `&`, `;`, `|`).
* Check if the filename contains `oxc_language_server` to ensure it's the expected binary.
*
* The check for malicious characters is not needed, but it's an additional layer of security.
* When using `shell: true` in `LanguageClient.ServerOptions`, it can be vulnerable to command injection.
* Even though we are not using `shell: true`, it's a good practice to validate the input.
*/
export function validateSafeBinaryPath(binary: string): boolean {
// Check for path traversal
if (binary.includes('..')) {
return false;
}

// Check for malicious characters or patterns
// These characters are never expected in a binary path.
// If any of these characters are present, we consider the path unsafe.
const maliciousPatterns = ['$', '&', ';', '|', '`', '>', '<', '!'];
for (const pattern of maliciousPatterns) {
if (binary.includes(pattern)) {
return false;
}
}
// Check if the filename contains `oxc_language_server`
// Malicious projects might try to point to a different binary.
if (!binary.includes('oxc_language_server')) {
return false;
}

return true;
}
8 changes: 4 additions & 4 deletions editors/vscode/tests/ConfigService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ suite('ConfigService', () => {

strictEqual(nonDefinedServerPath, undefined);

await conf.update('path.server', '/absolute/binary');
await conf.update('path.server', '/absolute/oxc_language_server');
const absoluteServerPath = service.getUserServerBinPath();

strictEqual(absoluteServerPath, '/absolute/binary');
strictEqual(absoluteServerPath, '/absolute/oxc_language_server');

await conf.update('path.server', './relative/binary');
await conf.update('path.server', './relative/oxc_language_server');
const relativeServerPath = service.getUserServerBinPath();

strictEqual(relativeServerPath, WORKSPACE_FOLDER.uri.path + '/relative/binary');
strictEqual(relativeServerPath, WORKSPACE_FOLDER.uri.path + '/relative/oxc_language_server');
});
});
36 changes: 36 additions & 0 deletions editors/vscode/tests/PathValidator.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { strictEqual } from 'assert';
import { validateSafeBinaryPath } from '../client/PathValidator';

suite('validateSafeBinaryPath', () => {
test('should return true for valid binary paths', () => {
strictEqual(validateSafeBinaryPath('/usr/local/bin/oxc_language_server'), true);
strictEqual(validateSafeBinaryPath('C:\\Program Files\\oxc_language_server.exe'), true);
strictEqual(validateSafeBinaryPath('./oxc_language_server'), true);
strictEqual(validateSafeBinaryPath('/opt/oxc_language_server'), true);
});

test('should reject paths with directory traversal', () => {
strictEqual(validateSafeBinaryPath('../oxc_language_server'), false);
strictEqual(validateSafeBinaryPath('../../oxc_language_server'), false);
strictEqual(validateSafeBinaryPath('/usr/local/../bin/oxc_language_server'), false);
strictEqual(validateSafeBinaryPath('..\\oxc_language_server'), false);
});

test('should reject paths with malicious characters', () => {
strictEqual(validateSafeBinaryPath('oxc_language_server;rm -rf /'), false);
strictEqual(validateSafeBinaryPath('oxc_language_server|cat /etc/passwd'), false);
strictEqual(validateSafeBinaryPath('oxc_language_server$PATH'), false);
strictEqual(validateSafeBinaryPath('oxc_language_server>output.txt'), false);
strictEqual(validateSafeBinaryPath('oxc_language_server<input.txt'), false);
strictEqual(validateSafeBinaryPath('oxc_language_server`whoami`'), false);
strictEqual(validateSafeBinaryPath('oxc_language_server!'), false);
});

test('should reject paths not containing oxc_language_server', () => {
strictEqual(validateSafeBinaryPath('/usr/local/bin/malicious'), false);
strictEqual(validateSafeBinaryPath('fake_server'), false);
strictEqual(validateSafeBinaryPath(''), false);
strictEqual(validateSafeBinaryPath('oxc_language'), false);
strictEqual(validateSafeBinaryPath('language_server'), false);
});
});
Loading