Skip to content
Draft
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
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 47 additions & 7 deletions book/src/workspace-trust.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,63 @@
# Workspace trust

Helix has a number of potentially dangerous features, namely LSP and ability to use local to workspace configurations. Those features can lead to unexpected code execution. To protect against code execution in dangerous contexts, Helix has a workspace trust protection, which will prevent these potentially dangerous features from running automatically.
Helix has a number of potentially dangerous features, namely LSP and ability to use workspace-local configuration files. Those features can lead to unexpected code execution. To protect against code execution in dangerous contexts, Helix has a workspace trust protection, which will prevent these potentially dangerous features from running automatically.

Helix will not trust any workspace by default.

By default, it will prompt about trust when you open new file in a workspace where you didn't make a decision about trust yet.
By default, it will prompt about trust when you open a new file in a workspace where you didn't make a decision about trust yet.

If you decide not to trust a workspace and don't want to be prompted about trust every time you start a new session in it, you can exclude the workspace by choosing `Never` option in trust selection window.
If you decide not to trust a workspace and don't want to be prompted about trust each session, you can exclude the workspace by choosing `Never` option in trust selection window.

You can always make current workspace trusted by running `:workspace-trust` command, and untrust it with `:workspace-untrust`.

Lists of trusted and excluded workspaces, delimited by newline characters, are stored in `~/.local/share/helix/trusted_workspaces` and `~/.local/share/helix/excluded_workspaces` correspondingly.
The list of trusted and permanently untrusted workspaces, delimited by newline characters, are stored in `~/.local/share/helix/trusted_workspaces` and `~/.local/share/helix/excluded_workspaces` respectively.
<!-- TODO: Windows paths -->

# Configuration

You can return to the old behaviour of loading every local `.helix/config.toml` and `.helix/languages.toml` and starting LSP's without an explicit permission by setting following option:
You can return to the old behaviour to accept the risk of loading every local `.helix/config.toml` and `.helix/languages.toml` and starting LSPs without an explicit permission by setting the following option:

```toml
[editor]
insecure = true
[editor.trust]
paths = [ "**" ]
```

In addition to trusting any workspaces with the wildcard glob `**`, it is possible to configure trust fine-grained with a `.gitignore` like syntax. E.g. consider the following:

```toml
[editor.trust]
paths = [
"~/repos/helix",
"~/repos/foo/*",
"~/repos/bar/**",
"!~/repos/bar/untrusted"
]
```

This would result in the following trust levels assuming the home directory `/home/user`:

| Path | Decision |
|:----------------------------------------------- |:------------------- |
| `/home/user/foobar` | undecided |
| `/home/user/repos/helix` | trusted |
| `/home/other/repos/helix` | undecided |
| `/home/user/repos/helix/branch_a` | undecided |
| `/home/user/repos/foo` | undecided |
| `/home/user/repos/foo/branch_a` | trusted |
| `/home/user/repos/foo/remote_a/branch_a` | undecided |
| `/home/user/repos/bar/branch_a` | trusted |
| `/home/user/repos/bar/remote_a/branch_a` | trusted |
| `/home/user/repos/bar/untrusted` | untrusted |

Specifically, the paths are processed one entry at a time by expanding a leading `~/` to the user's home directory, expanding a `*` to any single path segment, and expanding `**` to any number of path segments. An entry prefixed with `!` is a negated entry, instead of granting trust to a matching workspace it will deny it. After processing the full list, the most recently matched entry "wins". If no entry matched (e.g. because the default configuration with an empty list of `paths` was used), the user will be prompted as described in the beginning.

A secure configuration that will never prompt would be:

```toml
paths = [
"!**",
"~/repos/helix",
...
```

Above configuration will only grant trust to `~/repos/helix` and deny trust to any other path.
8 changes: 4 additions & 4 deletions helix-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ impl std::fmt::Display for LanguageLoaderError {
impl std::error::Error for LanguageLoaderError {}

/// Language configuration based on user configured languages.toml.
pub fn user_lang_config(insecure: bool) -> Result<Configuration, toml::de::Error> {
helix_loader::config::user_lang_config(insecure)?.try_into()
pub fn user_lang_config(config: &helix_loader::workspace_trust::Config) -> Result<Configuration, toml::de::Error> {
helix_loader::config::user_lang_config(config)?.try_into()
}

/// Language configuration loader based on user configured languages.toml.
pub fn user_lang_loader(insecure: bool) -> Result<Loader, LanguageLoaderError> {
let config_val = helix_loader::config::user_lang_config(insecure)
pub fn user_lang_loader(config: &helix_loader::workspace_trust::Config) -> Result<Loader, LanguageLoaderError> {
let config_val = helix_loader::config::user_lang_config(config)
.map_err(LanguageLoaderError::DeserializeError)?;
let config = config_val.clone().try_into().map_err(|e| {
if let Some(languages) = config_val.get("language").and_then(|v| v.as_array()) {
Expand Down
4 changes: 4 additions & 0 deletions helix-loader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ rust-version.workspace = true
categories.workspace = true
repository.workspace = true
homepage.workspace = true
globset.workspace = true

[[bin]]
name = "hx-loader"
Expand All @@ -23,6 +24,7 @@ toml.workspace = true
etcetera.workspace = true
once_cell = "1.21"
log = "0.4"
parking_lot.workspace = true

# TODO: these two should be on !wasm32 only

Expand All @@ -32,3 +34,5 @@ threadpool = { version = "1.0" }
tempfile.workspace = true

tree-house.workspace = true
globset.workspace = true
arc-swap.workspace = true
6 changes: 3 additions & 3 deletions helix-loader/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::str::from_utf8;

use crate::workspace_trust::{quick_query_workspace, TrustStatus};
use crate::workspace_trust::{self, quick_query_workspace, TrustStatus};

/// Default built-in languages.toml.
pub fn default_lang_config() -> toml::Value {
Expand All @@ -10,11 +10,11 @@ pub fn default_lang_config() -> toml::Value {
}

/// User configured languages.toml file, merged with the default config.
pub fn user_lang_config(insecure: bool) -> Result<toml::Value, toml::de::Error> {
pub fn user_lang_config(config: &workspace_trust::Config) -> Result<toml::Value, toml::de::Error> {
let global_config = crate::lang_config_file();
let workspace_config = crate::workspace_lang_config_file();

let files = if let TrustStatus::Trusted = quick_query_workspace(insecure) {
let files = if let TrustStatus::Trusted = quick_query_workspace(config) {
vec![global_config, workspace_config]
} else {
vec![global_config]
Expand Down
4 changes: 2 additions & 2 deletions helix-loader/src/grammar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ pub fn build_grammars(target: Option<String>) -> Result<()> {
// merged. The `grammar_selection` key of the config is then used to filter
// down all grammars into a subset of the user's choosing.
fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
let config: Configuration = crate::config::user_lang_config(false)
let config: Configuration = crate::config::user_lang_config(&crate::workspace_trust::Config::default())
.context("Could not parse languages.toml")?
.try_into()?;

Expand All @@ -217,7 +217,7 @@ fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
}

pub fn get_grammar_names() -> Result<Option<HashSet<String>>> {
let config: Configuration = crate::config::user_lang_config(false)
let config: Configuration = crate::config::user_lang_config(&crate::workspace_trust::Config::default())
.context("Could not parse languages.toml")?
.try_into()?;

Expand Down
Loading
Loading