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
81 changes: 81 additions & 0 deletions e2e/tasks/test_task_include_trust
Original file line number Diff line number Diff line change
@@ -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 <<EOF >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 <<EOF >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
14 changes: 13 additions & 1 deletion src/config/config_file/config_root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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"));
Expand All @@ -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"));
Expand Down
39 changes: 32 additions & 7 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -2399,8 +2399,10 @@ async fn load_tasks_includes(
config_root: &Path,
task_config_dir: &Option<String>,
templates: &IndexMap<String, TaskTemplate>,
require_trust: bool,
) -> Result<Vec<Task>> {
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)
Expand All @@ -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?,
);
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -2590,6 +2600,7 @@ pub async fn load_tasks_in_dir(
templates: &IndexMap<String, TaskTemplate>,
) -> Result<Vec<Task>> {
let configs = configs_at_root(dir, config_files);
let require_task_include_trust = configs.is_empty();

let (includes, resolve_dir) = configs
.iter()
Expand Down Expand Up @@ -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);
}
Expand All @@ -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<Config>,
path: &Path,
Expand Down
Loading