fix: eliminate command injection and unhandled JSON.parse in LSP tool
- config.ts: Replace execSync(`which ${command}`) with spawnSync("which", [command])
to prevent shell injection from malicious lsp.json config files
- client.ts: Wrap JSON.parse in parseMessage with try/catch and handle null messages
in the stream reader to prevent process crashes from malformed LSP output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
120ae367ad
commit
2c4f5de321
2 changed files with 19 additions and 11 deletions
|
|
@ -164,7 +164,7 @@ const CLIENT_CAPABILITIES = {
|
|||
|
||||
function parseMessage(
|
||||
buffer: Buffer,
|
||||
): { message: LspJsonRpcResponse | LspJsonRpcNotification; remaining: Buffer } | null {
|
||||
): { message: LspJsonRpcResponse | LspJsonRpcNotification | null; remaining: Buffer } | null {
|
||||
const headerEndIndex = findHeaderEnd(buffer);
|
||||
if (headerEndIndex === -1) return null;
|
||||
|
||||
|
|
@ -182,10 +182,15 @@ function parseMessage(
|
|||
const messageText = new TextDecoder().decode(messageBytes);
|
||||
const remaining = Buffer.from(buffer.subarray(messageEnd));
|
||||
|
||||
return {
|
||||
message: JSON.parse(messageText),
|
||||
remaining,
|
||||
};
|
||||
let message: LspJsonRpcResponse | LspJsonRpcNotification;
|
||||
try {
|
||||
message = JSON.parse(messageText);
|
||||
} catch {
|
||||
// Malformed JSON from LSP server — skip this message and advance past it
|
||||
return { message: null, remaining };
|
||||
}
|
||||
|
||||
return { message, remaining };
|
||||
}
|
||||
|
||||
function findHeaderEnd(buffer: Uint8Array): number {
|
||||
|
|
@ -239,6 +244,11 @@ async function startMessageReader(client: LspClient): Promise<void> {
|
|||
const { message, remaining } = parsed;
|
||||
workingBuffer = remaining;
|
||||
|
||||
if (!message) {
|
||||
parsed = parseMessage(workingBuffer);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("id" in message && message.id !== undefined) {
|
||||
const pending = client.pendingRequests.get(message.id);
|
||||
if (pending) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|||
import { createRequire } from "node:module";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { execSync } from "node:child_process";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import YAML from "yaml";
|
||||
import { globSync } from "glob";
|
||||
import { CONFIG_DIR_NAME } from "../../config.js";
|
||||
|
|
@ -177,11 +177,9 @@ const LOCAL_BIN_PATHS: Array<{ markers: string[]; binDir: string }> = [
|
|||
];
|
||||
|
||||
function which(command: string): string | null {
|
||||
try {
|
||||
return execSync(`which ${command}`, { encoding: "utf-8" }).trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
const result = spawnSync("which", [command], { encoding: "utf-8" });
|
||||
if (result.status !== 0) return null;
|
||||
return result.stdout.trim() || null;
|
||||
}
|
||||
|
||||
export function resolveCommand(command: string, cwd: string): string | null {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue