1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
//! LSP configuration types.
use std::collections::HashMap;
/// Top-level LSP configuration. Defaults to disabled with no servers.
#[derive(Clone, Debug, serde::Deserialize, Default)]
pub struct LspConfig {
/// Whether the LSP subsystem is active. Defaults to `false` (opt-in).
#[serde(default)]
pub enabled: bool,
/// Map of language id (e.g. `"rust"`) to server configuration.
#[serde(default)]
pub servers: HashMap<String, ServerConfig>,
}
/// Configuration for a single language server.
#[derive(Clone, Debug, serde::Deserialize)]
pub struct ServerConfig {
/// Executable name or path (e.g. `"rust-analyzer"`).
pub command: String,
/// Extra arguments passed to the server.
#[serde(default)]
pub args: Vec<String>,
/// Files/dirs whose presence marks the workspace root.
#[serde(default)]
pub root_markers: Vec<String>,
/// Shut down a server that has had no attached buffers for this many
/// seconds. `0` means never (Phase 1 keeps servers alive anyway).
#[serde(default)]
pub shutdown_idle_after_secs: u64,
/// Server-specific `initializationOptions` sent in the `initialize`
/// request. When unset, a sensible per-server default is applied (e.g.
/// rust-analyzer is configured to run `cargo clippy` on save). Set this in
/// config to override the default.
#[serde(default)]
pub initialization_options: Option<serde_json::Value>,
}
impl ServerConfig {
/// Validate the config before it is used to spawn a process.
///
/// # Security
///
/// `command` is **trusted user configuration** accessed from the user's
/// own config file. Absolute paths to language servers are legitimate;
/// bare names (e.g. `"rust-analyzer"`) rely on `$PATH` and are also
/// intentional — they match every editor's LSP config convention.
///
/// If **project-local config** (`.hjkl.toml`, `.editorconfig`, etc.) is
/// ever supported, this `validate` method MUST become the enforcement
/// point for untrusted-command restrictions. At minimum, that means:
///
/// * Reject the command when the config source is not the user's global
/// config (add a `trusted: bool` parameter or source-tracking).
/// * Reject relative paths (prevent `./malicious-binary` from a cloned
/// repo). Today relative paths are allowed because trusted user config
/// is the only source; that MUST change when project-local config lands.
/// * Consider an allowlist of known LSP binary names, or at minimum
/// reject commands containing path separators (`/`, `\\`) from
/// project-local sources.
///
/// For now, as a defense-in-depth measure even for user config, reject
/// commands with `..` path components — a typo like `"../bin/foo"` has
/// no legitimate use for a language server and is likely a path traversal.
pub fn validate(&self, language_id: &str) -> Result<(), String> {
if self.command.trim().is_empty() {
return Err(format!(
"lsp: server for {language_id:?} has an empty `command`"
));
}
// Defense-in-depth: reject `..` path traversal in the command even
// for trusted user config — no legitimate LSP binary lives in a
// parent directory relative to cwd.
if self.command.contains("..") {
return Err(format!(
"lsp: server command for {language_id:?} contains path traversal (`..`)"
));
}
Ok(())
}
}