From 306fe669cc545d202bacf4e83b2380748d802a3c Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Fri, 12 Jun 2026 15:54:54 +0000 Subject: [PATCH] fix(task): require trust for config-less task includes --- e2e/tasks/test_task_include_trust | 81 +++++++++++++++++++++++++++ src/config/config_file/config_root.rs | 14 ++++- src/config/mod.rs | 39 ++++++++++--- 3 files changed, 126 insertions(+), 8 deletions(-) create mode 100644 e2e/tasks/test_task_include_trust diff --git a/e2e/tasks/test_task_include_trust b/e2e/tasks/test_task_include_trust new file mode 100644 index 0000000000..075c235f00 --- /dev/null +++ b/e2e/tasks/test_task_include_trust @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +# Config-less task include directories must not render task templates before trust. +# This prevents arbitrary command execution from default task include files such as +# mise-tasks/*.toml in a freshly cloned repository. + +marker="$MISE_TMP_DIR/mise-task-include-trust-marker" +trap 'rm -f "$marker"' EXIT + +export MISE_TRUSTED_CONFIG_PATHS="" + +mkdir -p mise-tasks +cat <mise-tasks/ci.toml +[test] +description = "{{ exec(command='echo PWNED > $marker') }}" +run = "echo test" +EOF + +set +e +output=$(MISE_YES=0 MISE_PARANOID=1 mise tasks 2>&1) +status=$? +set -e + +if [[ $status -eq 0 ]]; then + echo "FAIL: Expected mise tasks to reject the untrusted task include" + echo "Output: $output" + exit 1 +fi + +if [[ -f $marker ]]; then + echo "FAIL: Tera exec() ran from an untrusted task include file" + echo "Output: $output" + exit 1 +fi + +if echo "$output" | grep -qi "not trusted"; then + echo "PASS: Untrusted task include file was blocked" +else + echo "FAIL: Expected trust-related error, got: $output" + exit 1 +fi + +rm -rf mise-tasks + +cat <<'EOF' >mise.toml +experimental_monorepo_root = true + +[monorepo] +config_roots = ["pkg"] +EOF + +mkdir -p pkg/mise-tasks +cat <pkg/mise-tasks/ci.toml +[test] +description = "{{ exec(command='echo PWNED > $marker') }}" +run = "echo test" +EOF + +set +e +output=$(MISE_YES=0 MISE_PARANOID=1 MISE_TRUSTED_CONFIG_PATHS="$PWD/mise.toml" mise tasks --all 2>&1) +status=$? +set -e + +if [[ $status -eq 0 ]]; then + echo "FAIL: Expected mise tasks --all to reject the untrusted monorepo task include" + echo "Output: $output" + exit 1 +fi + +if [[ -f $marker ]]; then + echo "FAIL: Tera exec() ran from an untrusted monorepo task include file" + echo "Output: $output" + exit 1 +fi + +if echo "$output" | grep -qi "not trusted"; then + echo "PASS: Untrusted monorepo task include file was blocked" +else + echo "FAIL: Expected trust-related monorepo error, got: $output" + exit 1 +fi diff --git a/src/config/config_file/config_root.rs b/src/config/config_file/config_root.rs index 28a1b19216..737f3a3d53 100644 --- a/src/config/config_file/config_root.rs +++ b/src/config/config_file/config_root.rs @@ -54,7 +54,9 @@ pub fn config_root(path: &Path) -> PathBuf { let is_config_filename = |f: &str| { f == "config.toml" || f == "config.local.toml" || regex!(r"config\..+\.toml").is_match(f) }; - let out = if parent == "conf.d" && is_mise_dir(grandparent) { + let out = if parent == "mise-tasks" || parent == ".mise-tasks" { + grandparent_path() + } else if (parent == "tasks" || parent == "conf.d") && is_mise_dir(grandparent) { if great_grandparent == ".config" { great_great_grandparent_path() } else { @@ -96,16 +98,21 @@ mod tests { "/foo/bar/.mise/conf.d/foo.toml", "/foo/bar/.mise/config.local.toml", "/foo/bar/.mise/config.toml", + "/foo/bar/.mise/tasks/build.toml", "/foo/bar/.tool-versions", "/foo/bar/mise.env.toml", "/foo/bar/mise.local.toml", "/foo/bar/mise.toml", "/foo/bar/mise/config.local.toml", "/foo/bar/mise/config.toml", + "/foo/bar/mise/tasks/build.toml", "/foo/bar/.config/mise/config.env.toml", "/foo/bar/.config/mise.env.toml", + "/foo/bar/.config/mise/tasks/build.toml", "/foo/bar/.mise/config.env.toml", "/foo/bar/.mise.env.toml", + "/foo/bar/.mise-tasks/build.toml", + "/foo/bar/mise-tasks/build.toml", ] { println!("{p}"); assert_eq!(config_root(Path::new(p)), PathBuf::from("/foo/bar")); @@ -128,16 +135,21 @@ mod tests { "/foo/mise/.mise/conf.d/foo.toml", "/foo/mise/.mise/config.local.toml", "/foo/mise/.mise/config.toml", + "/foo/mise/.mise/tasks/build.toml", "/foo/mise/.tool-versions", "/foo/mise/mise.env.toml", "/foo/mise/mise.local.toml", "/foo/mise/mise.toml", "/foo/mise/mise/config.local.toml", "/foo/mise/mise/config.toml", + "/foo/mise/mise/tasks/build.toml", "/foo/mise/.config/mise/config.env.toml", "/foo/mise/.config/mise.env.toml", + "/foo/mise/.config/mise/tasks/build.toml", "/foo/mise/.mise/config.env.toml", "/foo/mise/.mise.env.toml", + "/foo/mise/.mise-tasks/build.toml", + "/foo/mise/mise-tasks/build.toml", ] { println!("{p}"); assert_eq!(config_root(Path::new(p)), PathBuf::from("/foo/mise")); diff --git a/src/config/mod.rs b/src/config/mod.rs index 045823c958..48a8bd132a 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -20,7 +20,7 @@ use crate::cli::version; use crate::config::config_file::idiomatic_version::IdiomaticVersionFile; use crate::config::config_file::min_version::MinVersionSpec; use crate::config::config_file::mise_toml::{MiseToml, Tasks}; -use crate::config::config_file::{ConfigFile, config_trust_root}; +use crate::config::config_file::{ConfigFile, config_trust_root, trust_check}; use crate::config::env_directive::{EnvResolveOptions, EnvResults, ToolsFilter}; use crate::config::tracking::Tracker; use crate::env::{MISE_DEFAULT_CONFIG_FILENAME, MISE_DEFAULT_TOOL_VERSIONS_FILENAME}; @@ -2027,7 +2027,7 @@ async fn load_local_tasks_with_context( let includes = task_includes_for_dir(&subdir, &config.config_files)?; for include in includes { let mut subdir_tasks = load_tasks_includes( - &config, &include, &subdir, &None, &templates, + &config, &include, &subdir, &None, &templates, true, ) .await?; if is_global_task_include_path(&include) { @@ -2399,8 +2399,10 @@ async fn load_tasks_includes( config_root: &Path, task_config_dir: &Option, templates: &IndexMap, + require_trust: bool, ) -> Result> { if root.is_file() && root.extension().map(|e| e == "toml").unwrap_or(false) { + trust_check_task_include(root, require_trust)?; load_task_file(config, root, config_root, task_config_dir, templates).await } else if root.is_dir() { let all_files = WalkDir::new(root) @@ -2427,6 +2429,7 @@ async fn load_tasks_includes( .partition(|p| is_toml(p)); let mut tasks = vec![]; for path in toml_files { + trust_check_task_include(&path, require_trust)?; tasks.extend( load_task_file(config, &path, config_root, task_config_dir, templates).await?, ); @@ -2437,6 +2440,7 @@ async fn load_tasks_includes( let root = root.clone(); let config_root = config_root.clone(); let config = config.clone(); + trust_check_task_include(&path, require_trust)?; let mut task = Task::from_path(&config, &path, &root, &config_root).await?; if task.dir.is_none() && let Some(ref dir) = *task_config_dir @@ -2537,9 +2541,15 @@ async fn load_file_tasks( expand_task_include(&cf_root, &include) }; for path in paths { - let mut loaded = - load_tasks_includes(config, &path, &config_root, &task_config_dir, templates) - .await?; + let mut loaded = load_tasks_includes( + config, + &path, + &config_root, + &task_config_dir, + templates, + false, + ) + .await?; if is_global_task_include_path(&path) { mark_tasks_as_global(&mut loaded); } @@ -2590,6 +2600,7 @@ pub async fn load_tasks_in_dir( templates: &IndexMap, ) -> Result> { let configs = configs_at_root(dir, config_files); + let require_task_include_trust = configs.is_empty(); let (includes, resolve_dir) = configs .iter() @@ -2618,8 +2629,15 @@ pub async fn load_tasks_in_dir( expand_task_include(&resolve_dir, include) }; for p in paths { - let mut loaded = - load_tasks_includes(config, &p, dir, &task_config_dir, templates).await?; + let mut loaded = load_tasks_includes( + config, + &p, + dir, + &task_config_dir, + templates, + require_task_include_trust, + ) + .await?; if is_global_task_include_path(&p) { mark_tasks_as_global(&mut loaded); } @@ -2642,6 +2660,13 @@ pub async fn load_tasks_in_dir( Ok(tasks) } +fn trust_check_task_include(path: &Path, require_trust: bool) -> Result<()> { + if require_trust && !is_global_task_include_path(path) { + trust_check(path)?; + } + Ok(()) +} + async fn load_task_file( config: &Arc, path: &Path,